Realtime: Broadcast and Presence
The idea in one sentence
Section titled “The idea in one sentence”Broadcast and Presence are a general pub/sub and online-tracking layer on top of a Realtime channel, and unlike Postgres Changes they are enabled by default with no table or replication setup at all.
Broadcast: send arbitrary messages to a channel
Section titled “Broadcast: send arbitrary messages to a channel”Postgres Changes (covered in the previous lesson) only fires when a row actually changes in the database. Plenty of real-time features do not need — and should not pay the cost of — a database write at all: a live cursor position, a “user is typing” flag, or a chat message that only needs to reach people currently in the room. Broadcast covers exactly this: any client subscribed to a channel can send a JSON payload, and every other subscriber on that channel receives it directly, with no public table, no publication, and no WAL involved anywhere.
Sending a broadcast message from supabase-js:
const channel = supabase.channel('room1');
channel.on('broadcast', { event: 'cursor-pos' }, (payload) => { console.log('Cursor moved:', payload.payload);});
channel.subscribe();
channel.send({ type: 'broadcast', event: 'cursor-pos', payload: { x: 120, y: 340 },});Every subscriber that registered a .on('broadcast', { event: 'cursor-pos' }, ...) handler on room1 gets the payload, typically within tens of milliseconds — there is no database round trip on the hot path.
Presence: who is here right now
Section titled “Presence: who is here right now”Presence builds on the same channel mechanism to solve a different problem: knowing which clients are currently connected to a channel, and what state each one wants to share (a username, a cursor position, an “online” or “typing” flag). Instead of you manually broadcasting “I joined” and “I left” messages and reconciling them yourself, a channel with Presence enabled keeps a synchronized set of present clients and pushes updates to everyone subscribed whenever someone joins, leaves, or updates their tracked state.
A client announces its own presence with track, and reads the full set of currently present clients with presenceState:
const channel = supabase.channel('room1');
channel.on('presence', { event: 'sync' }, () => { const state = channel.presenceState(); console.log('Currently online:', state);});
channel.subscribe(async (status) => { if (status === 'SUBSCRIBED') { await channel.track({ user_id: 'user-42', online_at: new Date().toISOString() }); }});The sync event fires whenever the presence set changes for anyone on the channel, and presenceState() always returns the current full picture — keyed by presence key, with each entry holding whatever state that client tracked.
Neither one touches the database
Section titled “Neither one touches the database”This is the contrast worth holding onto after the previous lesson: Postgres Changes only works once you run alter publication supabase_realtime add table ... for a specific table, because it is fundamentally about streaming out of Postgres’s own replication log. Broadcast and Presence are unrelated to any of that — they are a messaging and state-sync layer that lives entirely inside the Realtime server, enabled the moment you connect a channel, with nothing to toggle in the database at all.
flowchart LR
subgraph channel["Realtime channel room1"]
c1["Client A"]
c2["Client B"]
end
c1 -->|"channel.send broadcast cursor-pos"| c2
c1 -->|"channel.track presence state"| sync["Synced presence state"]
c2 -->|"channel.track presence state"| sync
sync -->|"presence sync event"| c1
sync -->|"presence sync event"| c2