supabase-rb-rb
Realtime

Overview

Channels, subscriptions, broadcast, and presence.

The Realtime surface lets a Ruby process listen for Postgres changes, send broadcasts between connected clients, and track presence on a topic. Channels live on a single shared WebSocket; you open them with supabase.channel, attach listeners with channel.on_*, then call channel.subscribe to start the join handshake. Teardown is channel.unsubscribe (per-channel) or supabase.remove_channel / supabase.remove_all_channels (registry-level).

require "supabase"

supabase = Supabase.create_client(
  supabase_url: ENV.fetch("SUPABASE_URL"),
  supabase_key: ENV.fetch("SUPABASE_ANON_KEY")
)

channel = supabase
  .channel("public:countries")
  .on_postgres_changes("INSERT", schema: "public", table: "countries") do |payload|
    puts "new country: #{payload['data']['record']}"
  end
  .subscribe do |state, error|
    puts "subscribe state: #{state}"
  end

# ...later...
supabase.remove_channel(channel)   # or: channel.unsubscribe

Methods on the top-level client

MethodDescription
channelOpen (or re-open) a topic. Returns a Supabase::Realtime::Channel.
remove_channelUnsubscribe one channel and drop it from the registry. Closes the socket when the registry empties.
remove_all_channelsUnsubscribe every tracked channel and close the socket.
get_channelsSnapshot (Array<Channel>) of every channel currently tracked. Returns a dup so iteration is safe.

Methods on a channel

MethodDescription
on_postgres_changesListen to INSERT/UPDATE/DELETE (or "*") for a schema/table/filter.
on_broadcastListen to a named broadcast event sent by another client on this topic.
on_presence_sync / on_presence_join / on_presence_leaveMirror Phoenix Presence state changes into your code.
subscribeStart the join handshake. Optional block fires with SUBSCRIBED / CHANNEL_ERROR / TIMED_OUT.
unsubscribeSend a phx_leave. State stays in LEAVING until the server acks.
send_broadcast(event, payload)Emit a broadcast frame to every other subscriber of the topic.
track(payload) / untrackMutate this client's presence entry. See the on page for end-to-end examples.
push_event(event, payload, timeout:)Public low-level Phoenix push for custom events. Returns the Push so you can attach receive(...) blocks.
presence_stateSnapshot of the current { key => [presences] } map.
on_close(&block) / on_error(&block)Channel-level lifecycle hooks (Ruby-only).

Every realtime method is synchronous

Heartbeat, read-loop, and reconnect run on background Threads, and thread-safety is provided by explicit mutexes (Presence, Push, Timer, send-buffer). Listener blocks (on_postgres_changes, on_broadcast, presence callbacks, subscribe's status block) run on the read-thread — a slow callback delays delivery of the next frame. Exceptions raised inside a callback are caught by Supabase::Realtime::CallbackSafety and logged so the read-loop survives.

Sockets, reconnect, and on_reconnect_failed

Supabase::Realtime::Client owns one Socket. The production transport is Sockets::WebsocketClientSimple (constructed lazily when no transport: is injected). When the socket drops, a background thread reconnects with exponential backoff — initial_backoff * 2^(attempts - 1) seconds, capped at 60s, up to max_retries attempts. When all attempts are exhausted, the registered on_reconnect_failed callback fires with the last underlying error.

supabase.realtime.on_reconnect_failed do |error|
  Rails.logger.error("realtime gave up after retries: #{error&.message}")
end

This callback fires once the client has given up after exhausting all retries.

Subscribe states

The block passed to subscribe is invoked with one of:

StateMeaning
"SUBSCRIBED"The server acked the join. Listeners are now live.
"CHANNEL_ERROR"The server replied with an error (e.g. RLS rejected the join, or on_postgres_changes bindings didn't round-trip).
"TIMED_OUT"No reply within Types::DEFAULT_TIMEOUT_SECONDS (10s).
"CLOSED"Server-side phx_close after a successful join.

These come from Supabase::Realtime::Types::SubscribeStates.

On this page