# Broadcast

> Send low-latency messages between clients with Supabase Realtime Broadcast on SupaHost, from the browser, from a server over REST, or from the database with realtime.send().

Source: https://supahost.app/docs/realtime/broadcast · Updated 2026-11-11

Broadcast sends a message to every client subscribed to a channel. Messages sent by clients go through your project's Realtime server without being stored, so they suit things that only matter right now: chat messages being typed, cursor positions, live reactions, game moves, or a nudge telling clients to reload some data.

You can send Broadcast messages from three places: a connected client, any server over HTTP, or the database itself.

## Listen and send from the client

A channel is identified by its topic name. Anyone subscribed to the same topic in the same project receives the same messages. Each message has an `event` name, so one channel can carry several kinds of messages.

This example is a team chat that shows typing indicators.

```ts
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!)

const room = supabase.channel('chat:design-team', {
  config: {
    broadcast: {
      self: false, // don't receive your own messages (the default)
      ack: true,   // wait for the server to confirm each send
    },
  },
})

room
  .on('broadcast', { event: 'typing' }, ({ payload }) => {
    showTyping(payload.userName)
  })
  .on('broadcast', { event: 'stopped-typing' }, ({ payload }) => {
    hideTyping(payload.userName)
  })
  .subscribe()

export async function notifyTyping(userName: string) {
  const result = await room.send({
    type: 'broadcast',
    event: 'typing',
    payload: { userName },
  })
  if (result !== 'ok') console.warn('Broadcast failed:', result) // 'timed out' or 'error'
}
```

Options:

- **`self: true`** delivers your own messages back to you. Handy when every client, including the sender, renders from the same handler.
- **`ack: true`** makes `send()` resolve only after Realtime has received the message. Without it, `send()` resolves straight away.

Use `'*'` as the event to receive every event on the channel: `.on('broadcast', { event: '*' }, handler)`.

Keep payloads small. Broadcast is designed for short messages, and every message to every subscriber counts toward the project's rate limits and your instance's egress. Send an id and let clients fetch details if they need more.

## Send from a server without a WebSocket

Server code often needs to announce something ("invoice 1042 was paid") without staying connected. There are two ways.

With supabase-js 2.76.0 or later, `httpSend()` posts the message over HTTP:

```ts
// In an API route or Edge Function
const channel = supabase.channel('team:7')
await channel.httpSend('invoice-paid', { invoiceId: 1042 })
```

From any language, call the Broadcast REST endpoint on your project:

```bash
curl -X POST 'https://app-one.acme.supahost.space/realtime/v1/api/broadcast' \
  -H "apikey: $SUPABASE_SERVICE_ROLE_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "messages": [
      { "topic": "team:7", "event": "invoice-paid", "payload": { "invoiceId": 1042 } }
    ]
  }'
```

One request can carry several messages for different topics. Use the `service_role` key only from server code. To send to a private channel, add `"private": true` to the message; the sender's token must then pass your `realtime.messages` insert policies (the `service_role` key bypasses them).

## Send from the database

Realtime adds a `realtime.send()` function to your database. Calling it from SQL broadcasts a message, which is useful in triggers, database functions or [cron jobs](/docs/cron-queues/cron).

```sql
select realtime.send(
  jsonb_build_object('invoiceId', 1042, 'status', 'paid'), -- payload
  'invoice-paid',                                           -- event
  'team:7',                                                 -- topic
  true                                                      -- private channel
);
```

The last argument must match the channel type on the client. A private message only reaches clients that joined with `private: true`, and a public message only reaches public channels.

A trigger can broadcast every change to a table. Here each new chat message is sent to its room's topic:

```sql
create or replace function public.broadcast_new_message()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
  perform realtime.send(
    jsonb_build_object('id', new.id, 'body', new.body, 'author_id', new.author_id),
    'new-message',
    'room:' || new.room_id::text,
    true
  );
  return null;
end;
$$;

create trigger messages_broadcast
after insert on public.messages
for each row execute function public.broadcast_new_message();
```

There's also `realtime.broadcast_changes()`, which formats a message from a trigger's `TG_OP`, `NEW` and `OLD` values for you. Messages sent from the database go through the `realtime.messages` table, so private topics need authorization policies. See [Realtime authorization](/docs/realtime/authorization) and [Broadcasting from the database](/kb/realtime-broadcast-from-database).

Broadcasting from the database is the better choice than [Postgres Changes](/docs/realtime/postgres-changes) when many clients watch the same busy table. Each change is sent once to a topic, instead of being checked against every subscriber.

## Clean up channels

Each client can join at most 100 channels. In single-page apps, remove channels when a view unmounts, or they pile up:

```ts
useEffect(() => {
  const room = supabase.channel(`chat:${roomId}`)
    .on('broadcast', { event: 'typing' }, handleTyping)
    .subscribe()
  return () => { supabase.removeChannel(room) }
}, [roomId])
```

If you see a `TooManyChannels` error, a component is probably creating channels without removing them. See [TooManyChannels error](/kb/realtime-too-many-channels).

## Delivery guarantees

Broadcast is fire and forget. A client that is disconnected when a message is sent from another client doesn't receive it later. For anything important, write it to a table first, then broadcast so connected clients update right away. Clients that reconnect can load what they missed from the table.

## Related

- [Presence](/docs/realtime/presence)
- [Realtime authorization](/docs/realtime/authorization)
- [Realtime messages not arriving](/kb/realtime-messages-not-arriving)
- [Broadcast reference in Supabase's docs](https://supabase.com/docs/guides/realtime/broadcast)
