Subscribe to a channel
Start the Realtime join handshake for a channel.
Kick off the phx_join handshake for a channel. subscribe switches the channel state to JOINING, serializes every registered on_postgres_changes listener into the join payload, and either sends the join (when the socket is already open) or opens the socket (the client's rejoin_channels then sends the join exactly once on on_open). The optional block fires when the join resolves — with SUBSCRIBED on success, CHANNEL_ERROR on a server error, or TIMED_OUT after Types::DEFAULT_TIMEOUT_SECONDS (10s) with no reply.
Signature
channel.subscribe do |state, error|case statewhen "SUBSCRIBED" then # channel is livewhen "CHANNEL_ERROR" then # error is the server payload (or a RealtimeError)when "TIMED_OUT" then # error is nilwhen "CLOSED" then # server-side phx_close after a joinendendsubscribe can only be called once per channel — a second call raises Supabase::Realtime::Errors::AlreadyJoinedError. To rejoin after unsubscribe, create a new channel via channel.
Parameters
This method takes no positional arguments. The block is optional.
| Name | Type | Required | Description |
|---|---|---|---|
block | Proc | Optional | Called once per state transition: SUBSCRIBED, CHANNEL_ERROR, TIMED_OUT, CLOSED. Block arity is |state, error| — state is a String from Types::SubscribeStates, error is the server payload / Errors::RealtimeError for CHANNEL_ERROR, nil otherwise. |
Returns
Returns the channel so the call can chain with .on_* listeners. The join is in flight when this method returns — use the block parameter (or on_close / on_error) to observe completion. State changes synchronously to JOINING; it only flips to JOINED after the server's phx_reply arrives on the read-thread.
Example — basic subscribe with status callback
channel = supabase
.channel("public:countries")
.on_postgres_changes("*", schema: "public", table: "countries") do |payload|
handle(payload)
end
.subscribe do |state, error|
case state
when "SUBSCRIBED"
puts "live"
when "CHANNEL_ERROR"
warn "join failed: #{error.inspect}"
when "TIMED_OUT"
warn "no phx_reply in 10s — server unreachable?"
end
endExample — subscribe without a callback
When you don't care about the join result, drop the block. State is observable later via channel.joined?, channel.errored?, etc.
channel = supabase.channel("room:42").subscribe
sleep 0.5 until channel.joined?
channel.send_broadcast("ping", { at: Time.now.utc.iso8601 })Example — wait synchronously for SUBSCRIBED
A blocking wait for SUBSCRIBED with a Queue-based handshake. Useful in test setups or one-shot scripts.
ready = Queue.new
channel = supabase
.channel("public:orders")
.on_postgres_changes("INSERT", schema: "public", table: "orders") { |p| handle(p) }
.subscribe { |state, error| ready.push([state, error]) }
state, error = ready.pop
raise "subscribe failed: #{error.inspect}" unless state == "SUBSCRIBED"Example — subscribe before the socket is open
subscribe is a one-call entry point. If the socket isn't connected yet, it triggers socket.connect (idempotent) and the join is sent automatically when on_open fires. Don't call socket.connect separately first — the client guards against duplicate joins (which the server would phx_close).
supabase = Supabase.create_client(supabase_url: ..., supabase_key: ...)
# Socket is NOT open yet.
supabase
.channel("room:42")
.on_broadcast("chat:message") { |p| puts p }
.subscribe { |state, _| puts "state=#{state}" }
# Socket opens, join fires, state becomes "SUBSCRIBED".Lifecycle
JOINING→ server replies withphx_reply(status: "ok") →JOINED, block called withSUBSCRIBED.JOINING→ server replies withphx_reply(status: "error") →ERRORED, block called withCHANNEL_ERROR. The rejoin timer (2^triesbackoff) schedules a retry.JOINING→ no reply within 10s →ERRORED, block called withTIMED_OUT. Rejoin timer same as above.- Once
JOINED, a server-sidephx_close(e.g. another tab opened the same topic) tears the channel down — the registeredon_closehooks fire and the channel is removed fromclient.channels. - On socket reconnect,
client.rejoin_channelsre-sends the join for every channel inJOINEDorJOININGstate. Channels inLEAVING/CLOSEDare left alone — rejoining them would silently revive a subscription the caller explicitly tore down.
Block-form subscribe; status block runs on read-thread
subscribe takes a block (channel.subscribe { |state, error| ... }) and is fully synchronous. There is no async surface for subscribe — under async: true the other sub-clients (auth / postgrest / etc.) are swapped, but realtime stays threaded. The status block runs on the read-thread; don't do long-running work in it (push to a Queue instead). Exceptions raised inside the block are caught by CallbackSafety and forwarded to the client's logger: — they will not crash the read-loop.