supabase-rb-rb
Realtime

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 joinendend

subscribe 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.

NameTypeRequiredDescription
blockProcOptionalCalled 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
self (Supabase::Realtime::Channel)

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
  end

Example — 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 with phx_reply (status: "ok") → JOINED, block called with SUBSCRIBED.
  • JOINING → server replies with phx_reply (status: "error") → ERRORED, block called with CHANNEL_ERROR. The rejoin timer (2^tries backoff) schedules a retry.
  • JOINING → no reply within 10s → ERRORED, block called with TIMED_OUT. Rejoin timer same as above.
  • Once JOINED, a server-side phx_close (e.g. another tab opened the same topic) tears the channel down — the registered on_close hooks fire and the channel is removed from client.channels.
  • On socket reconnect, client.rejoin_channels re-sends the join for every channel in JOINED or JOINING state. Channels in LEAVING / CLOSED are 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.

On this page