Skip to main content

Overview

Channels are the core concept in Supabase Realtime. They provide isolated communication streams for different features of your application.

Creating Channels

Basic Channel

Create a channel with a unique name:
const channel = supabase.channel('room-1')
name
string
required
Unique identifier for the channel. Can include letters, numbers, and hyphens.

Channel with Options

Configure channel behavior:
const channel = supabase.channel('room-1', {
  config: {
    broadcast: { self: true },
    presence: { key: 'user-id' }
  }
})
options.config.broadcast
object
Broadcast configuration.
options.config.presence
object
Presence configuration.

Subscribing to Channels

Subscribe

Activate the channel connection:
const channel = supabase
  .channel('room-1')
  .subscribe((status) => {
    if (status === 'SUBSCRIBED') {
      console.log('Channel is ready!')
    }
  })

Subscribe with Error Handling

const channel = supabase
  .channel('room-1')
  .subscribe((status, err) => {
    switch (status) {
      case 'SUBSCRIBED':
        console.log('Successfully subscribed to channel')
        break
      case 'TIMED_OUT':
        console.error('Connection timed out')
        break
      case 'CLOSED':
        console.log('Channel closed')
        break
      case 'CHANNEL_ERROR':
        console.error('Channel error:', err)
        break
    }
  })

Channel States

Connection States

SUBSCRIBED
status
Channel is connected and ready.
TIMED_OUT
status
Connection attempt timed out.
CLOSED
status
Channel has been closed.
CHANNEL_ERROR
status
An error occurred with the channel.

Check Channel State

const state = channel.state
console.log('Channel state:', state)

Unsubscribing

Remove Single Channel

await supabase.removeChannel(channel)

Remove All Channels

await supabase.removeAllChannels()

Unsubscribe in React

import { useEffect } from 'react'

function MyComponent() {
  useEffect(() => {
    const channel = supabase
      .channel('room-1')
      .subscribe()
    
    // Cleanup on component unmount
    return () => {
      supabase.removeChannel(channel)
    }
  }, [])
  
  return <div>My Component</div>
}

Channel Naming

Best Practices

// Good - descriptive and specific
supabase.channel('chat-room-123')
supabase.channel('user-notifications-456')
supabase.channel('game-lobby-xyz')

// Avoid - too generic
supabase.channel('channel1')
supabase.channel('test')

Scoped Channels

Organize channels by feature and ID:
// Chat rooms
const chatChannel = supabase.channel(`chat:${roomId}`)

// User-specific notifications
const notifChannel = supabase.channel(`notifications:${userId}`)

// Game sessions
const gameChannel = supabase.channel(`game:${sessionId}`)

Multiple Listeners

Add multiple event listeners to a single channel:
const channel = supabase
  .channel('multi-feature')
  // Listen to database changes
  .on(
    'postgres_changes',
    { event: '*', schema: 'public', table: 'messages' },
    (payload) => console.log('Message:', payload)
  )
  // Listen to broadcasts
  .on(
    'broadcast',
    { event: 'typing' },
    (payload) => console.log('Typing:', payload)
  )
  // Listen to presence
  .on(
    'presence',
    { event: 'sync' },
    () => console.log('Presence synced')
  )
  .subscribe()

Private Channels

Implement private channels using RLS:
-- Only allow users to join their own channels
CREATE POLICY "Users can only subscribe to own channel"
  ON realtime.messages
  FOR SELECT
  USING (
    channel_name = 'user:' || auth.uid()::text
  );
// Client code
const userId = session.user.id
const channel = supabase.channel(`user:${userId}`)

Channel Events

System Events

Listen to channel lifecycle events:
const channel = supabase.channel('room-1')

channel.on('system', {}, (payload) => {
  console.log('System event:', payload)
})

Broadcasting to Channels

Send Messages

Broadcast messages to all subscribers:
const channel = supabase.channel('room-1')

await channel.send({
  type: 'broadcast',
  event: 'message',
  payload: { text: 'Hello everyone!' }
})

Broadcast with Acknowledgment

const channel = supabase.channel('room-1', {
  config: { broadcast: { ack: true } }
})

const response = await channel.send({
  type: 'broadcast',
  event: 'message',
  payload: { text: 'Hello!' }
})

if (response === 'ok') {
  console.log('Message delivered')
}

Performance Optimization

Throttle Updates

Limit the rate of updates:
import { throttle } from 'lodash'

const sendPosition = throttle((x, y) => {
  channel.send({
    type: 'broadcast',
    event: 'cursor-pos',
    payload: { x, y }
  })
}, 100) // Maximum once per 100ms

Debounce Updates

Delay updates until activity stops:
import { debounce } from 'lodash'

const sendTyping = debounce(() => {
  channel.send({
    type: 'broadcast',
    event: 'typing',
    payload: { user: 'user-1' }
  })
}, 500) // Wait 500ms after last keystroke

Error Handling

Handle Connection Errors

const channel = supabase
  .channel('room-1')
  .subscribe((status, err) => {
    if (status === 'CHANNEL_ERROR') {
      console.error('Failed to connect:', err)
      // Implement retry logic
      setTimeout(() => {
        channel.subscribe()
      }, 5000)
    }
  })

Handle Timeout

let timeoutId

const channel = supabase
  .channel('room-1')
  .subscribe((status) => {
    if (status === 'SUBSCRIBED') {
      clearTimeout(timeoutId)
    } else if (status === 'TIMED_OUT') {
      console.error('Connection timed out, retrying...')
      timeoutId = setTimeout(() => {
        channel.subscribe()
      }, 3000)
    }
  })

Channel Patterns

Chat Room

function createChatRoom(roomId) {
  const channel = supabase
    .channel(`chat:${roomId}`)
    .on(
      'postgres_changes',
      { event: 'INSERT', schema: 'public', table: 'messages', filter: `room_id=eq.${roomId}` },
      (payload) => {
        displayMessage(payload.new)
      }
    )
    .on(
      'broadcast',
      { event: 'typing' },
      (payload) => {
        showTypingIndicator(payload.user)
      }
    )
    .on(
      'presence',
      { event: 'sync' },
      () => {
        updateOnlineUsers(channel.presenceState())
      }
    )
    .subscribe()
  
  return channel
}

Collaborative Editing

function createCollaborativeEditor(documentId) {
  const channel = supabase
    .channel(`doc:${documentId}`)
    .on(
      'broadcast',
      { event: 'cursor' },
      (payload) => {
        updateRemoteCursor(payload)
      }
    )
    .on(
      'broadcast',
      { event: 'change' },
      (payload) => {
        applyRemoteChange(payload)
      }
    )
    .on(
      'presence',
      { event: 'sync' },
      () => {
        updateCollaborators(channel.presenceState())
      }
    )
    .subscribe(async (status) => {
      if (status === 'SUBSCRIBED') {
        await channel.track({
          user_id: currentUser.id,
          name: currentUser.name
        })
      }
    })
  
  return channel
}

Live Dashboard

function createLiveDashboard() {
  const channel = supabase
    .channel('analytics-dashboard')
    .on(
      'postgres_changes',
      { event: '*', schema: 'public', table: 'analytics_events' },
      (payload) => {
        updateDashboard(payload)
      }
    )
    .subscribe()
  
  return channel
}

Best Practices

Name channels based on their purpose and scope:
supabase.channel(`chat-room-${roomId}`)
supabase.channel(`user-notifications-${userId}`)
Always unsubscribe when components unmount:
useEffect(() => {
  const channel = supabase.channel('my-channel').subscribe()
  return () => supabase.removeChannel(channel)
}, [])
Handle disconnections gracefully with retry logic.
Use throttling or debouncing for cursor positions, typing indicators, etc.
Implement Row Level Security to control channel access.

Next Steps

Subscriptions

Learn about subscription patterns

Client Libraries

Use Realtime with client SDKs