# Realtime

Realtime pushes messages to your connected users without them polling the server: client-to-client broadcast, presence (who is online), and broadcasting a table's changes. It speaks the `supabase-js` protocol. This page explains how to enable it, how to use it, what is not served, and the quotas of each tier.

## What is served

- **Broadcast**: a client sends a message on a channel, the other subscribers receive it. It goes over the WebSocket, or over REST (`POST /realtime/v1/api/broadcast`), the `supabase-js` fallback.
- **Presence**: each client announces its state, and everyone sees who is there.
- **Broadcast from the database**: a function sets a trigger on a table, and every insert, update or delete is broadcast on a channel.
- **Private channels**, judged by the policies you write on `realtime.messages`.

The WebSocket is `wss://<identifier>.donnees.bastide.app/realtime/v1/websocket`, protocols 1.0.0 and 2.0.0 (see [Database](database.md) for the address).

> **Attention.** Listening to database changes through `postgres_changes` is not served: such a subscription is refused when joining the channel (status `error`), and the refusal names broadcast from the database (LIM-08). No logical replication is exposed.

## Enabling realtime

Realtime is off by default. While it is off, `/realtime/v1` answers 403, code `temps_reel_inactif`.

- **Console**: open **Données** (Data), section « Temps réel » (realtime), then « Activer le temps réel » (enable realtime). The card shows your tier's quotas.
- **Agent**: `backend_realtime_configure` with `active: true` (rank `maintainer`, scope `db:write`).
- **Command line**: `bastide backend realtime configure --project-id <project> --active true`.
- **API**: `PUT /v1/projects/{projectId}/backend/realtime`.

```json
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "active": true,
  "idempotencyKey": "temps-reel-2026-10-28"
}
```

Enabling and disabling take effect within 30 seconds. Disabling closes the project's open connections, without touching other projects. The state and quotas can be read back with `backend_read` (the `tempsReel` field).

## Broadcasting and tracking presence

```js
const channel = supabase.channel("room-42");

channel
  .on("broadcast", { event: "cursor" }, ({ payload }) => draw(payload))
  .on("presence", { event: "sync" }, () => console.log(channel.presenceState()))
  .subscribe(async (status) => {
    if (status === "SUBSCRIBED") await channel.track({ name: "Lea" });
  });

await channel.send({ type: "broadcast", event: "cursor", payload: { x: 10, y: 20 } });
```

A channel topic starts with `realtime:` in the protocol; `supabase-js` adds it for you.

## Broadcasting a table's changes

Instead of `postgres_changes`, set the trigger once, in a migration or in the SQL editor:

```sql
select realtime.diffuser_les_changements('public.messages');
```

The `public:messages` channel then receives every insert, update and delete on the table, as broadcast. Messages coming from the database count against the project's messages-per-second quota; beyond it, they are dropped.

## Private channels

A private channel admits only the clients your policies admit. Write them on the `realtime.messages` table, with `realtime.topic()` (the channel's topic) and `auth.uid()`:

- the `SELECT` policy decides who can **join and read** the channel;
- the `INSERT` policy decides who can **send** to it.

The platform tests the policy inside a transaction that is always rolled back, under the caller's role: PostgreSQL judges it, exactly as you wrote it. See [Data security](data-security.md).

## Quotas by tier

| Tier | Messages per second | Concurrent connections |
| --- | --- | --- |
| Discovery | 100 | 200 |
| Standard | 500 | 2,000 |
| Pro | 2,500 | 10,000 |
| Sovereign | negotiated, no cap enforced | negotiated, no cap enforced |

On every tier: 100 channels per connection, a payload of at most 3,000 KB per message, and 5 presence calls per client per 30-second window.

- **Connections**: the slot is reserved before the handshake. Beyond the quota, the WebSocket gets 429, code `connexions_simultanees_depassees`, without any connection being opened.
- **Messages**: the quota is counted per full second, for the whole project (broadcasts, presence and database messages). Beyond it, the send gets an error naming the quota; over REST, 429.
- **Channels**: beyond 100 on one connection, joining is refused; leave one, or open another connection.

> **Note.** Realtime holds, per platform instance, one session-mode connection to your database while a client is connected. It counts against your tier's server connection ceiling (three on Discovery; see [Database](database.md)).

## Common errors

- **403 `temps_reel_inactif`**: enable realtime for the project.
- **Subscription refused, with a reason pointing to broadcast from the database**: you asked for `postgres_changes`; use `realtime.diffuser_les_changements`.
- **429 `connexions_simultanees_depassees`**: close unused connections, or change tier.
- **« jeton d'accès refusé »** (access token refused) when joining: the session token expired; reconnect the client.
- **Nothing arrives on a private channel**: the `SELECT` policy on `realtime.messages` does not admit this user.

## See also

- [Database](database.md), [Data security](data-security.md), [Authentication](auth.md), [Limits by tier](limits.md)
- Reference: [backend.realtime.configure](../reference/operations/backend.realtime.configure.md)
