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), thesupabase-jsfallback. - 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 for the address).
Attention. Listening to database changes through
postgres_changesis not served: such a subscription is refused when joining the channel (statuserror), 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_configurewithactive: true(rankmaintainer, scopedb:write). - Command line:
bastide backend realtime configure --project-id <project> --active true. - API:
PUT /v1/projects/{projectId}/backend/realtime.
{
"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
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:
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
SELECTpolicy decides who can join and read the channel; - the
INSERTpolicy 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.
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).
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; userealtime.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
SELECTpolicy onrealtime.messagesdoes not admit this user.