# Broadcast

Send low-latency messages using the client libs, REST, or your Database.

You can use Realtime Broadcast to send low-latency messages between users. Messages can be sent using the client libraries, REST APIs, or directly from your database.

## How Broadcast works

The way Broadcast works changes based on the channel you are using:

- **REST API**: Receives an HTTP request and then sends a message via WebSocket to connected clients
- **Client libraries**: Sends a message via WebSocket to the server, and then the server sends a message via WebSocket to connected clients
- **Database**: Adds a new entry to `realtime.messages` where a logical replication is set to listen for changes, and then sends a message via WebSocket to connected clients

Note: The public flag (the last argument in `realtime.send(payload, event, topic, is_private)`) only affects who can subscribe to the topic not who can read messages from the database.

- Public (`false`) → Anyone can subscribe to that topic without authentication
- Private (`true`) → Only authenticated clients can subscribe to that topic

Regardless if it's public or private, the Realtime service connects to your database as the authenticated Supabase Admin role.

For Authorization, we insert a message and try to read it, and rollback the transaction to verify that the Row Level Security (RLS) policies set by the user are being respected by the user joining the channel, but this message isn't sent to the user. You can read more about it in [Authorization](/docs/guides/realtime/authorization).

## Subscribe to messages

You can use the Supabase client libraries to receive Broadcast messages.

### Initialize the client

Get the Project URL and key from [the project's **Connect** dialog](/dashboard/project/_?showConnect=true).

Deprecation: Supabase is deprecating the `anon` and `service_role` keys by the end of 2026. Use the publishable (`sb_publishable_xxx`) and secret (`sb_secret_xxx`) keys instead. For the reasoning behind the change, see [the announcement on GitHub](https://github.com/orgs/supabase/discussions/29260).

In most cases you can get keys from your project's [**Connect** dialog](/dashboard/project/_?showConnect=true\&connectTab=\&framework=). For every way to retrieve a key, including the CLI and the Management API, refer to [Find your keys](/docs/guides/getting-started/api-keys#find-your-keys).

**JavaScript**

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

const SUPABASE_URL = 'https://<project>.supabase.co'
const SUPABASE_KEY = '<sb_publishable_... key>'

const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
```

**Dart**

```dart
import 'package:supabase_flutter/supabase_flutter.dart';

void main() async {
  Supabase.initialize(
    url: 'https://<project>.supabase.co',
    publishableKey: '<sb_publishable_... key>',
  );
  runApp(MyApp());
}

final supabase = Supabase.instance.client;
```

**Swift**

```swift
import Supabase

let SUPABASE_URL = "https://<project>.supabase.co"
let SUPABASE_KEY = "<sb_publishable_... key>"

let supabase = SupabaseClient(supabaseURL: URL(string: SUPABASE_URL)!, supabaseKey: SUPABASE_KEY)
```

**Kotlin**

```kotlin
val supabaseUrl = "https://<project>.supabase.co"
val supabaseKey = "<sb_publishable_... key>"
val supabase = createSupabaseClient(supabaseUrl, supabaseKey) {
    install(Realtime)
}
```

**Python**

```python
import asyncio
from supabase import acreate_client

URL = "https://<project>.supabase.co"
KEY = "<sb_publishable_... key>"

async def create_supabase():
  supabase = await acreate_client(URL, KEY)
  return supabase
```

**C#**

```c#
var supabase = new Supabase.Client(
    "https://<project>.supabase.co",
    "<sb_publishable_... key>"
);
await supabase.InitializeAsync();
```

### Receive Broadcast messages

You can receive Broadcast messages by providing a callback to the channel.

Note: Binary payloads (`ArrayBuffer` / `ArrayBufferView`) are received automatically from **supabase-js 2.91.0** and **supabase-swift 2.44.0**. On older SDK versions, binary messages are silently dropped and never reach the callback.

**JavaScript**

```js
// @noImplicitAny: false
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://<project>.supabase.co', '<sb_publishable_... key>')

// ---cut---
// Join a room/topic. Can be anything except for 'realtime'.
const myChannel = supabase.channel('test-channel')

// Function to log any messages we receive
function messageReceived(payload) {
  console.log(payload)
}

// Subscribe to the Channel
myChannel
  .on(
    'broadcast',
    { event: 'shout' }, // Listen for "shout". Can be "*" to listen to all events
    (payload) => messageReceived(payload)
  )
  .subscribe()
```

**Dart**

```dart
final myChannel = supabase.channel('test-channel');

// Log any messages we receive
void messageReceived(payload) {
  print(payload);
}

// Subscribe to the Channel
myChannel
    .onBroadcast(
        event: 'shout', // Listen for "shout". Can be "*" to listen to all events
        callback: (payload) => messageReceived(payload)
    )
    .subscribe();
```

**Swift**

```swift
let myChannel = await supabase.channel("test-channel")

// Listen for broadcast messages
let broadcastStream = await myChannel.broadcast(event: "shout") // Listen for "shout". Can be "*" to listen to all events

await myChannel.subscribe()

for await event in broadcastStream {
  print(event)
}
```

**Kotlin**

```kotlin
val myChannel = supabase.channel("test-channel")

/ Listen for broadcast messages
val broadcastFlow: Flow<JsonObject> = myChannel
    .broadcastFlow<JsonObject>("shout") // Listen for "shout". Can be "*" to listen to all events
    .onEach { println(it) }
    .launchIn(yourCoroutineScope) // you can also use .collect { } here

myChannel.subscribe()
```

**Python**

Note: In the following Realtime examples, certain methods are awaited. These should be enclosed within an `async` function.

```python
# Join a room/topic. Can be anything except for 'realtime'.
my_channel = supabase.channel('test-channel')

# Function to log any messages we receive
def message_received(payload):
  print(f"Broadcast received: {payload}")

# Subscribe to the Channel
await my_channel
  .on_broadcast('shout', message_received) # Listen for "shout". Can be "*" to listen to all events
  .subscribe()
```

**C#**

```c#
class ShoutBroadcast : BaseBroadcast
{
    [JsonProperty("message")]
    public string Message { get; set; }
}

// Join a room/topic. Can be anything except for 'realtime'.
var myChannel = supabase.Realtime.Channel("test-channel");

// Register a typed broadcast and log any messages we receive
var broadcast = myChannel.Register<ShoutBroadcast>();
broadcast.AddBroadcastEventHandler((sender, _) =>
{
    Console.WriteLine(broadcast.Current());
});

// Subscribe to the channel
await myChannel.Subscribe();
```

## Send messages

### Broadcast using the client libraries

You can use the Supabase client libraries to send Broadcast messages.

Note: Broadcast payloads can be binary (`ArrayBuffer` or `ArrayBufferView`, e.g. `Uint8Array`) over WebSocket from **supabase-js 2.91.0** and **supabase-swift 2.44.0**. Binary payloads sent to clients running older SDK versions are **silently dropped** and never arrive over the WebSocket. The Dart, Kotlin, and Python clients don't support binary payloads yet.

**JavaScript**

```js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('your_project_url', 'your_supabase_api_key')

// ---cut---
const myChannel = supabase.channel('test-channel')

/**
 * Sending a message before subscribing will use HTTP
 */
myChannel
  .send({
    type: 'broadcast',
    event: 'shout',
    payload: { message: 'Hi' },
  })
  .then((resp) => console.log(resp))


/**
 * Sending a message after subscribing will use WebSockets
 */
myChannel.subscribe((status) => {
  if (status !== 'SUBSCRIBED') {
    return null
  }

  myChannel.send({
    type: 'broadcast',
    event: 'shout',
    payload: { message: 'Hi' },
  })
})

/**
 * The payload can be binary (ArrayBuffer / ArrayBufferView) from supabase-js 2.91.0.
 * Receivers on older SDK versions will not get the message.
 */
myChannel.send({
  type: 'broadcast',
  event: 'cursor-pos',
  payload: new Uint8Array([1, 2, 3]).buffer,
})
```

**Dart**

```dart
final myChannel = supabase.channel('test-channel');

// Sending a message before subscribing will use HTTP
final res = await myChannel.sendBroadcastMessage(
  event: "shout",
  payload: { 'message': 'Hi' },
);
print(res);

// Sending a message after subscribing will use WebSockets
myChannel.subscribe((status, error) {
  if (status != RealtimeSubscribeStatus.subscribed) {
    return;
  }

  myChannel.sendBroadcastMessage(
    event: 'shout',
    payload: { 'message': 'hello, world' },
  );
});
```

**Swift**

Note: Binary payloads over WebSocket are supported from supabase-swift 2.44.0. Receivers on older SDK versions will not get binary messages.

```swift
let myChannel = await supabase.channel("test-channel") {
  $0.broadcast.acknowledgeBroadcasts = true
}

// Sending a message before subscribing will use HTTP
await myChannel.broadcast(event: "shout", message: ["message": "HI"])

// Sending a message after subscribing will use WebSockets
await myChannel.subscribe()
try await myChannel.broadcast(
    event: "shout",
    message: YourMessage(message: "hello, world!")
)
```

**Kotlin**

```kotlin
val myChannel = supabase.channel("test-channel") {
  broadcast {
    acknowledgeBroadcasts = true
  }
}

// Sending a message before subscribing will use HTTP
myChannel.broadcast(event = "shout", buildJsonObject {
  put("message", "Hi")
})

// Sending a message after subscribing will use WebSockets
myChannel.subscribe(blockUntilSubscribed = true)
channelB.broadcast(
  event = "shout",
  payload = YourMessage(message = "hello, world!")
)
```

**Python**

Note: When an asynchronous method needs to be used within a synchronous context, such as the callback for `.subscribe()`, use `asyncio.create_task()` to schedule the coroutine. This is why the [initialize the client](#initialize-the-client) example includes an import of `asyncio`.

```python
my_channel = supabase.channel('test-channel')

# Sending a message after subscribing will use WebSockets
def on_subscribe(status, err):
  if status != RealtimeSubscribeStates.SUBSCRIBED:
    return

  asyncio.create_task(my_channel.send_broadcast(
    'shout',
    { "message": 'hello, world' },
  ))

await my_channel.subscribe(on_subscribe)
```

**C#**

```c#
var myChannel = supabase.Realtime.Channel("test-channel");
var broadcast = myChannel.Register<ShoutBroadcast>();

await myChannel.Subscribe();

// Send a broadcast message over the WebSocket
await broadcast.Send("shout", new ShoutBroadcast { Message = "Hi" });
```

### Broadcast from the Database

Note: All the messages sent using Broadcast from the Database are stored in `realtime.messages` table and will be deleted after 3 days.

You can send messages directly from your database using the `realtime.send()` function:

```sql
select
  realtime.send(
    jsonb_build_object('hello', 'world'), -- JSONB Payload
    'event', -- Event name
    'topic', -- Topic
    false    -- Private flag - true: Private (default), false: Public
  );
```

Note: The `realtime.send()` function in the database includes a flag that determines whether the broadcast is private or public, and client channels also have the same configuration. For broadcasts to work correctly, these settings must match. A public broadcast only reaches public channels and a private broadcast only reaches private channels.

By default, all database broadcasts are private, meaning clients must authenticate to receive them. If the database sends a public message but the client subscribes to a private channel, the message is not delivered because private channels only accept signed, authenticated messages.

To broadcast a binary payload from your database, use the `realtime.send_binary()` function with a `bytea` payload:

```sql
select
  realtime.send_binary(
    '\x012345'::bytea, -- bytea payload
    'event',           -- Event name
    'topic',           -- Topic
    true               -- Private flag - true: Private (default), false: Public
  );
```

The same public/private matching rule applies: a binary broadcast only reaches channels with the same private setting. Binary messages only reach clients on **supabase-js 2.91.0** and **supabase-swift 2.44.0** or later; older clients silently drop them.

You can use the `realtime.broadcast_changes()` helper function to broadcast messages when a record is created, updated, or deleted. For more details, read [Subscribing to Database Changes](/docs/guides/realtime/subscribing-to-database-changes).

### Broadcast using the REST API

You can send a single Broadcast message by making an HTTP request to Realtime servers. The endpoint embeds the topic and event in the path, and the `Content-Type` header determines the payload type:

- `application/json` — JSON payload
- `application/octet-stream` — binary payload

Add `?private=true` to broadcast to a private channel.

**cURL**

```bash
# JSON payload
curl -v \
-H 'apikey: <SUPABASE_TOKEN>' \
-H 'Content-Type: application/json' \
--data-raw '{ "test": "test" }' \
'https://<PROJECT_REF>.supabase.co/realtime/v1/api/broadcast/test/events/event'

# Binary payload
curl -v \
-H 'apikey: <SUPABASE_TOKEN>' \
-H 'Content-Type: application/octet-stream' \
--data-binary @payload.bin \
'https://<PROJECT_REF>.supabase.co/realtime/v1/api/broadcast/test/events/event?private=true'
```

**POST**

```bash
POST /realtime/v1/api/broadcast/test/events/event HTTP/1.1
Host: {PROJECT_REF}.supabase.co
Content-Type: application/json
apikey: {SUPABASE_TOKEN}
{ "test": "test" }
```

Note: To send multiple messages in a single request, the batch endpoint `POST /realtime/v1/api/broadcast` is still available. It accepts a JSON body with a `messages` array (JSON payloads only):

```bash
curl -v \
-H 'apikey: <SUPABASE_TOKEN>' \
-H 'Content-Type: application/json' \
--data-raw '{
  "messages": [
    {
      "topic": "test",
      "event": "event",
      "payload": { "test": "test" }
    }
  ]
}' \
'https://<PROJECT_REF>.supabase.co/realtime/v1/api/broadcast'
```

## Broadcast options

You can pass configuration options while initializing the Supabase Client.

### Self-send messages

**JavaScript**

By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting Broadcast's `self` parameter to `true`.

```js
const myChannel = supabase.channel('room-2', {
  config: {
    broadcast: { self: true },
  },
})

myChannel.on(
  'broadcast',
  { event: 'test-my-messages' },
  (payload) => console.log(payload)
)

myChannel.subscribe((status) => {
  if (status !== 'SUBSCRIBED') { return }
  myChannel.send({
    type: 'broadcast',
    event: 'test-my-messages',
    payload: { message: 'talking to myself' },
  })
})
```

**Dart**

By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting Broadcast's `self` parameter to `true`.

```dart
final myChannel = supabase.channel(
  'room-2',
  opts: const RealtimeChannelConfig(
    self: true,
  ),
);

myChannel.onBroadcast(
  event: 'test-my-messages',
  callback: (payload) => print(payload),
);

myChannel.subscribe((status, error) {
  if (status != RealtimeSubscribeStatus.subscribed) return;
  // channelC.send({
  myChannel.sendBroadcastMessage(
    event: 'test-my-messages',
    payload: {'message': 'talking to myself'},
  );
});
```

**Swift**

By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting Broadcast's `receiveOwnBroadcasts` parameter to `true`.

```swift
let myChannel = await supabase.channel("room-2") {
  $0.broadcast.receiveOwnBroadcasts = true
}

let broadcastStream = await myChannel.broadcast(event: "test-my-messages")

await myChannel.subscribe()

try await myChannel.broadcast(
    event: "test-my-messages",
    payload: YourMessage(
        message: "talking to myself"
    )
)
```

**Kotlin**

By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting Broadcast's `receiveOwnBroadcasts` parameter to `true`.

```kotlin
val myChannel = supabase.channel("room-2") {
    broadcast {
        receiveOwnBroadcasts = true
    }
}

val broadcastFlow: Flow<JsonObject> = myChannel.broadcastFlow<JsonObject>("test-my-messages")
    .onEach {
        println(it)
    }
    .launchIn(yourCoroutineScope)

myChannel.subscribe(blockUntilSubscribed = true) //You can also use the myChannel.status flow instead, but this parameter will block the coroutine until the status is joined.

myChannel.broadcast(
    event = "test-my-messages",
    payload = YourMessage(
        message = "talking to myself"
    )
)
```

**Python**

Note: When an asynchronous method needs to be used within a synchronous context, such as the callback for `.subscribe()`, use `asyncio.create_task()` to schedule the coroutine. This is why the [initialize the client](#initialize-the-client) example includes an import of `asyncio`.

By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting Broadcast's `self` parameter to `True`.

```python
# Join a room/topic. Can be anything except for 'realtime'.
my_channel = supabase.channel('room-2', {"config": {"broadcast": {"self": True}}})

my_channel.on_broadcast(
  'test-my-messages',
  lambda payload: print(payload)
)

def on_subscribe(status, err):
  if status != RealtimeSubscribeStates.SUBSCRIBED:
    return

  # Send a message once the client is subscribed
  asyncio.create_task(channel_b.send_broadcast(
    'test-my-messages',
    { "message": 'talking to myself' },
  ))

my_channel.subscribe(on_subscribe)
```

**C#**

By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting the `broadcastSelf` parameter to `true`.

```c#
var myChannel = supabase.Realtime.Channel("room-2");
var broadcast = myChannel.Register<ShoutBroadcast>(broadcastSelf: true);
broadcast.AddBroadcastEventHandler((sender, _) =>
{
    Console.WriteLine(broadcast.Current());
});

await myChannel.Subscribe();

await broadcast.Send("test-my-messages", new ShoutBroadcast { Message = "talking to myself" });
```

### Acknowledge messages

**JavaScript**

You can confirm that the Realtime servers have received your message by setting Broadcast's `ack` setting to `true`.

```js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('your_project_url', 'your_supabase_api_key')

// ---cut---
const myChannel = supabase.channel('room-3', {
  config: {
    broadcast: { ack: true },
  },
})

myChannel.subscribe(async (status) => {
  if (status !== 'SUBSCRIBED') { return }

  const serverResponse = await myChannel.send({
    type: 'broadcast',
    event: 'acknowledge',
    payload: {},
  })

  console.log('serverResponse', serverResponse)
})
```

**Dart**

```dart
final myChannel = supabase.channel('room-3',opts: const RealtimeChannelConfig(
  ack: true,
),

);

myChannel.subscribe( (status, error) async {
  if (status != RealtimeSubscribeStatus.subscribed) return;

  final serverResponse = await myChannel.sendBroadcastMessage(

    event: 'acknowledge',
    payload: {},
  );

  print('serverResponse: $serverResponse');
});
```

**Swift**

You can confirm that Realtime received your message by setting Broadcast's `acknowledgeBroadcasts` config to `true`.

```swift
let myChannel = await supabase.channel("room-3") {
  $0.broadcast.acknowledgeBroadcasts = true
}

await myChannel.subscribe()

await myChannel.broadcast(event: "acknowledge", message: [:])
```

**Kotlin**

By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting Broadcast's `acknowledgeBroadcasts` parameter to `true`.

```kotlin
val myChannel = supabase.channel("room-2") {
    broadcast {
        acknowledgeBroadcasts = true
    }
}

myChannel.subscribe(blockUntilSubscribed = true) //You can also use the myChannel.status flow instead, but this parameter will block the coroutine until the status is joined.

myChannel.broadcast(event = "acknowledge", buildJsonObject {  })
```

**Python**

Unsupported in Python yet.

**C#**

You can confirm that the Realtime servers have received your message by setting the `broadcastAck` parameter to `true`. `Send` then returns `true` once the server acknowledges the message.

```c#
var myChannel = supabase.Realtime.Channel("room-3");
var broadcast = myChannel.Register<ShoutBroadcast>(broadcastAck: true);

await myChannel.Subscribe();

var acknowledged = await broadcast.Send("acknowledge", new ShoutBroadcast { Message = "Hi" });
Console.WriteLine(acknowledged);
```

Use this to guarantee that the server has received the message before resolving `channelD.send`'s promise. If the `ack` config is not set to `true` when creating the channel, the promise returned by `channelD.send` will resolve immediately.

### Send messages using REST calls

You can also send a Broadcast message by making an HTTP request to Realtime servers. This is useful when you want to send messages from your server or client without having to first establish a WebSocket connection.

**JavaScript**

Note: `channel.httpSend()` always uses the REST API regardless of WebSocket connection state, and is available from the Supabase JavaScript client version 2.107.0 and later. `ArrayBuffer` and `ArrayBufferView` (e.g. `Uint8Array`) payloads are sent as `application/octet-stream`; all other payloads are JSON-encoded.

```js
const channel = supabase.channel('test-channel')

// No need to subscribe to channel

// JSON payload
await channel.httpSend('cursor-pos', { x: Math.random(), y: Math.random() })

// Binary payload (ArrayBuffer / ArrayBufferView) — sent as application/octet-stream
await channel.httpSend('cursor-pos', new Uint8Array([1, 2, 3]).buffer)

// Remember to clean up the channel

supabase.removeChannel(channel)

```

**Dart**

```dart
// No need to subscribe to channel

final channel = supabase.channel('test-channel');
final res = await channel.sendBroadcastMessage(
  event: "test",
  payload: {
    'message': 'Hi',
  },
);
print(res);
```

**Swift**

```swift
let myChannel = await supabase.channel("room-2") {
  $0.broadcast.acknowledgeBroadcasts = true
}

// No need to subscribe to channel

await myChannel.broadcast(event: "test", message: ["message": "HI"])
```

**Kotlin**

```kotlin
val myChannel = supabase.channel("room-2") {
    broadcast {
        acknowledgeBroadcasts = true
    }
}

// No need to subscribe to channel

myChannel.broadcast(event = "test", buildJsonObject {
    put("message", "Hi")
})
```

**Python**

Unsupported in Python yet.

## Trigger broadcast messages from your database

### How it works

Broadcast Changes allows you to trigger messages from your database. To achieve it, Realtime directly reads your Write-Ahead Log (WAL) file using a publication against the `realtime.messages` table. Whenever a new insert occurs, a message is sent to connected users.

It uses partitioned tables per day, which allows performant deletion of your previous messages by dropping the physical tables of this partitioned table. Tables older than 3 days are deleted.

A client connecting over WebSocket creates those daily partitions. `realtime.send` does not. With no partition for today, the insert fails and Postgres logs a `WarnSendingBroadcastMessage` warning, so connect a client before you broadcast from the database. See [Realtime: `WarnSendingBroadcastMessage`](/docs/guides/troubleshooting/realtime-warn-sending-broadcast-message) if you hit that warning.

Broadcasting from the database works like a client-side broadcast, using WebSockets to send JSON payloads. [Realtime Authorization](/docs/guides/realtime/authorization) is required and enabled by default to protect your data.

Broadcast Changes provides two functions to help you send messages:

- `realtime.send()` inserts a message into `realtime.messages` without a specific format.
- `realtime.broadcast_changes()` inserts a message with the required fields to emit database changes to clients. This helps you set up triggers on your tables to emit changes.

### Broadcasting a message from your database

The `realtime.send()` function provides the most flexibility by allowing you to broadcast messages from your database without a specific format. This allows you to use database broadcast for messages that aren't necessarily tied to the shape of a Postgres row change.

```sql
SELECT realtime.send (
	'{}'::jsonb, -- JSONB Payload
	'event', -- Event name
	'topic', -- Topic
	FALSE -- Public / Private flag
);
```

### Broadcast record changes

#### Setup realtime authorization

Realtime Authorization is required and enabled by default. To allow your users to listen to messages from topics, create an RLS policy:

```sql
CREATE POLICY "authenticated can receive broadcasts"
ON "realtime"."messages"
FOR SELECT
TO authenticated
USING ( true );

```

Read [Realtime Authorization](/docs/guides/realtime/authorization) to learn how to set up more specific policies.

#### Set up trigger function

First, set up a trigger function that uses the `realtime.broadcast_changes()` function to insert an event whenever it is triggered. The event is set up to include data on the schema, table, operation, and field changes that triggered it.

For this example, you're going broadcast events to a topic named `topic:<record_id>`.

```sql
CREATE OR REPLACE FUNCTION public.your_table_changes()
RETURNS trigger
SECURITY DEFINER SET search_path = ''
AS $$
BEGIN
    PERFORM realtime.broadcast_changes(
	    'topic:' || NEW.id::text,   -- topic
		   TG_OP,                          -- event
		   TG_OP,                          -- operation
		   TG_TABLE_NAME,                  -- table
		   TG_TABLE_SCHEMA,                -- schema
		   NEW,                            -- new record
		   OLD                             -- old record
		);
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;
```

The Postgres native trigger special variables used are:

- `TG_OP` - the operation that triggered the function
- `TG_TABLE_NAME` - the table that caused the trigger
- `TG_TABLE_SCHEMA` - the schema of the table that caused the trigger invocation
- `NEW` - the record after the change
- `OLD` - the record before the change

You can read more about them in this [guide](https://www.postgresql.org/docs/current/plpgsql-trigger.html#PLPGSQL-DML-TRIGGER).

#### Set up trigger

Next, set up a trigger so the function runs whenever your target table has a change.

```sql
CREATE TRIGGER broadcast_changes_for_your_table_trigger
AFTER INSERT OR UPDATE OR DELETE ON public.your_table
FOR EACH ROW
EXECUTE FUNCTION your_table_changes ();
```

As you can see, it will be broadcasting all operations so our users will receive events when records are inserted, updated or deleted from `public.your_table` .

#### Listen on client side

Finally, client side will requires to be set up to listen to the topic `topic:<record id>` to receive the events.

```jsx
const gameId = 'id'
await supabase.realtime.setAuth() // Needed for Realtime Authorization
const changes = supabase
  .channel(`topic:${gameId}`)
  .on('broadcast', { event: 'INSERT' }, (payload) => console.log(payload))
  .on('broadcast', { event: 'UPDATE' }, (payload) => console.log(payload))
  .on('broadcast', { event: 'DELETE' }, (payload) => console.log(payload))
  .subscribe()
```

## Broadcast replay

### How it works

Broadcast Replay enables **private** channels to access messages that were sent earlier. Only messages published via [Broadcast From the Database](#broadcast-from-the-database) are available for replay.

You can configure replay with the following options:

- **`since`** (Required): The epoch timestamp in milliseconds (for example, `1697472000000`), specifying the earliest point from which messages should be retrieved.
- **`limit`** (Optional): The number of messages to return. This must be a positive integer, with a maximum value of 25.

Note: Messages are stored in daily partitions, and partitions older than 72 hours are dropped. Because whole days are removed at once, a message stays available for at least 72 hours and at most 4 days, depending on the time of day it was sent. Setting `since` further back than the retained window does not recover deleted messages. See [Realtime Limits](/docs/guides/realtime/limits) for details.

**JavaScript**

Note: This is currently available only in the Supabase JavaScript client version 2.74.0 and later.

```js
const config = {
  private: true,
  broadcast: {
    replay: {
      since: 1697472000000, // Unix timestamp in milliseconds
      limit: 10
    }
  }
}
const channel = supabase.channel('main:room', { config })

// Broadcast callback receives meta field
channel.on('broadcast', { event: 'position' }, (payload) => {
  if (payload?.meta?.replayed) {
    console.log('Replayed message: ', payload)
  } else {
    console.log('This is a new message', payload)
  }
  // ...
})
.subscribe()
```

**Dart**

Note: This is currently available only in the Supabase Dart client version 2.10.0 and later.

```dart
// Configure broadcast with replay
final channel = supabase.channel(
  'my-channel',
  RealtimeChannelConfig(
    self: true,
    ack: true,
    private: true,
    replay: ReplayOption(
      since: 1697472000000, // Unix timestamp in milliseconds
      limit: 25,
    ),
  ),
);

// Broadcast callback receives meta field
channel.onBroadcast(
  event: 'position',
  callback: (payload) {
    final meta = payload['meta'] as Map<String, dynamic>?;
    if (meta?['replayed'] == true) {
      print('Replayed message: ${meta?['id']}');
    }
  },
).subscribe();
```

**Swift**

Note: This is currently available only in the Supabase Swift client version 2.34.0 and later.

```swift
// Configure broadcast with replay
let channel = supabase.realtimeV2.channel("my-channel") {
  $0.isPrivate = true
  $0.broadcast.acknowledgeBroadcasts = true
  $0.broadcast.receiveOwnBroadcasts = true
  $0.broadcast.replay = ReplayOption(
    since: 1697472000000, // Unix timestamp in milliseconds
    limit: 25
  )
}

var subscriptions = Set<RealtimeSubscription>()

// Broadcast callback receives meta field
channel.onBroadcast(event: "position") { message in
  if let meta = message["payload"]?.objectValue?["meta"]?.objectValue,
     let replayed = meta["replayed"]?.boolValue,
     replayed {
    print("Replayed message: \(meta["id"]?.stringValue ?? "")")
  }
}
.store(in: &subscriptions)

await channel.subscribe()
```

**Kotlin**

Note: Unsupported in Kotlin for now.

**Python**

Note: This is currently available only in the Supabase Python client version 2.22.0 and later.

```python
# Configure broadcast with replay
channel = client.channel('my-channel', {
    'config': {
        "private": True,
        'broadcast': {
            'self': True,
            'ack': True,
            'replay': {
                'since': 1697472000000,
                'limit': 100
            }
        }
    }
})

# Broadcast callback receives meta field
def on_broadcast(payload):
    if payload.get('meta', {}).get('replayed'):
        print(f"Replayed message: {payload['meta']['id']}")

await channel.on_broadcast('position', on_broadcast)
await channel.subscribe()
```

#### When to use Broadcast replay

A few common use cases for Broadcast Replay include:

- Displaying the most recent messages from a chat room
- Loading the last events that happened during a sports event
- Ensuring users always see the latest events after a page reload or network interruption
- Highlighting the most recent sections that changed in a web page
