Setup
All three recipes share the same connection setup. We’re using a tiny WebSocket helper rather than the upstream@supabase/realtime-js library because Powabase doesn’t ship a first-party SDK yet — once you have a working WS client, the protocol from there is straightforward.
vsn=1.0.0 parameter tells Realtime which protocol version to use. The frames are Phoenix Channels JSON envelopes: { topic, event, payload, ref }. The ref is your client-generated request ID — Realtime echoes it back so you can correlate replies.
Recipe 1 — Live-updating list with Postgres Changes
A todo list where INSERT/UPDATE/DELETE onpublic.todos automatically updates the UI. The classic Postgres Changes pattern.
Step 1: enable Realtime for the table (one-time setup, in SQL):
public.todos so the channel only forwards the user’s own rows. This is the same own-rows pattern from the RLS Cookbook:
phx_join frame opens the subscription. The postgres_changes config tells Realtime which events to forward; you can add a filter field for column equality (e.g., filter: "user_id=eq.<uuid>") but in this case the RLS policy already restricts to the user’s own rows, so an extra filter is redundant.
Step 4: handle reconnects. If the WebSocket drops mid-session, your client loses everything that happened during the gap. The pattern is to refetch on reconnect — assume the local state may be stale and reissue the original “load all todos” query, then resume the subscription. Realtime is not a durable queue.
Recipe 2 — Presence: who’s online
For a collaborative document where you show avatars of everyone currently viewing the page. Each user broadcasts their own presence state; everyone sees the union.user_id client-side. Second, untrack only fires when the tab closes cleanly. If the user kills the tab or loses connectivity, Realtime infers the leave from a heartbeat timeout (about 60 seconds), so the avatar lingers briefly.
For “real-time cursor positions on top of a shared document,” combine Presence (to know who’s online) with Broadcast (to send the position updates without storing them). Each cursor move emits a broadcast event with { user_id, x, y }; the receivers update each user’s cursor based on the latest position they’ve seen.
Recipe 3 — Broadcast chat
A chat room where every message goes to every subscriber. No persistence — refresh the page and the history is gone. Real apps would also write messages to a table for history, but the broadcast pattern is the right starting point.self: false means the sender doesn’t receive their own messages — usually what you want, since you’re already showing them the message you sent. ack: true means Realtime sends a phx_reply confirming the broadcast was delivered to its routing layer (doesn’t confirm individual subscribers received it).
For a private chat room (only authenticated users in the room can read messages), wrap the join in a private channel:
realtime.topic() returns the channel name being checked; we split it on : and look up membership. The pattern is straight out of the RLS Cookbook — Realtime is just one more table to write policies against.
REST broadcast (sending from a backend)
For server-side message emission — webhook handlers, scheduled jobs — use the REST broadcast endpoint instead of opening a WebSocket from your backend:Common failure modes
403 TenantNotFoundon WS upgrade. Only happens with self-hosted Realtime where Kong isn’t preserving the Host header. On Powabase managed cloud, you should never see this. If you do, file a support ticket.401immediately afterphx_joinon a private channel. Your RLS policy onrealtime.messagesdenied the join. Test the policy in psql with the role+claims set, same as the RLS Testing flow.- Joined OK but no
postgres_changesevents arrive. Either (a) the table isn’t in thesupabase_realtimepublication, (b) RLS on the underlying table denies SELECT for your role, or (c) the filter syntax is wrong. Check the publication first:SELECT * FROM pg_publication_tables WHERE pubname = 'supabase_realtime';. - WebSocket closes after 60 seconds of silence. You’re not sending heartbeats. Add the
setIntervalfrom the recipes above. - Reconnect storms after a deploy. Realtime pods restart during deploys; clients reconnect immediately and pile on. Add exponential backoff with jitter to your reconnect logic — start at 1s, double up to 30s, jitter ±25%.
Next steps
Realtime model
The three channel types, auth, and the publication-setup gotcha.
Realtime Reference
WebSocket protocol details and the REST broadcast endpoint.
RLS Cookbook
The patterns that gate private channels (via realtime.messages RLS) and underlying tables for Postgres Changes.
Connection pooling
Why you can’t use LISTEN/NOTIFY as a Realtime alternative through the pooler.