supabase-rb-rb
Realtime

Create a channel

Open a Supabase Realtime channel on the shared WebSocket connection.

Open a channel on a topic. The call is cheap — it creates a Supabase::Realtime::Channel instance, registers it in client.channels, and returns it. No socket I/O happens until you attach listeners and call subscribe.

Signature

channel = supabase.channel(topic, params: nil)

The same method lives on the realtime sub-client directly (supabase.realtime.channel(topic, params: nil)) — the top-level shortcut just forwards.

Parameters

NameTypeRequiredDescription
topicStringRequiredTopic name. Auto-prefixed with realtime: when missing, so "public:users" and "realtime:public:users" reach the same channel.
paramsHashOptionalJoin-payload override. Defaults to a config hash with broadcast { ack: false, self: false }, presence { key: "", enabled: false }, private: false. Pass your own to enable broadcast acks, presence keys, or private (RLS-gated) channels.

Returns

Returns
Supabase::Realtime::Channel

A channel scoped to topic. Chain on_postgres_changes / on_broadcast / on_presence_*, then call subscribe to start the join handshake. The channel is added to client.get_channels immediately — the registry tracks intent, not connection state.

Example — open a channel and attach a Postgres-changes listener

channel = supabase
  .channel("public:countries")
  .on_postgres_changes("*", schema: "public", table: "countries") do |payload|
    puts payload["data"]["type"], payload["data"]["record"]
  end
  .subscribe

Example — auto-prefixing

Topic names get a realtime: prefix automatically. The two calls below open the same topic — pre-prefixed names are passed through unchanged.

supabase.channel("public:users")           # → "realtime:public:users"
supabase.channel("realtime:public:users")  # → "realtime:public:users"

Example — broadcast acks and presence key

Override params to ask the server to ack broadcasts and to enable Presence with a stable key (the key groups all metas from this client under the same map entry).

channel = supabase.channel("room:42", params: {
  "config" => {
    "broadcast" => { "ack" => true, "self" => false },
    "presence"  => { "key" => "user-#{current_user.id}", "enabled" => true },
    "private"   => false
  }
})

Example — private channels (RLS-gated)

Pass config.private: true so the server enforces the topic's realtime.messages policies against the JWT carried in the join payload. The current access_token (from set_auth, or the anon key on initial create) is automatically placed at the root of the join payload — you don't need to add it to params.

private_channel = supabase.channel("room:tenant-42", params: {
  "config" => {
    "broadcast" => { "ack" => false, "self" => false },
    "presence"  => { "key" => "", "enabled" => false },
    "private"   => true
  }
})

Multiple channels on the same topic

The registry is a flat list — client.channel(topic) always returns a new instance, even when a channel for the same topic already exists. Each gets its own join_ref / subscription lifecycle. To look up an existing channel, walk supabase.get_channels.find { |c| c.topic == "realtime:public:users" }.

Synchronous surface

The entire channel surface is synchronous — channel returns a fully usable object, and every method you call on it blocks the calling thread for the duration of the socket write. Heartbeat / read-loop / reconnect run on background Threads; if you want non-blocking realtime calls from the top-level client, see remove_channel for the async: true Async::Task shape.

On this page