Client events
A client event travels from one browser to the others subscribed to the same channel without passing through your backend. The sender calls trigger on a channel it is already on, the edge fans the event out to everyone else there, and your server does not see the request at all unless you ask for a webhook.
That is a good trade for signals that stop mattering a second later: a typing indicator, a cursor position, "still scrolling" heartbeats inside a room. You skip the round trip through your API, and you skip writing an endpoint whose only job would be to forward the message straight back out.
It is a bad trade for anything your app treats as true. Nothing validates a client event on the way through, so a client can send whatever it likes, whenever it likes, and the recipients have no way to tell. Chat messages you intend to store, state changes, anything with a permission attached: those still go through your server and out again with Publishing events.
Turn the setting on
Toggle Client Events for the app on the Realtime → Apps page, or set client_events to true on the app through the Realtime API. It applies to the whole app. Until it is on, the edge refuses every client event with an error telling you to enable the feature in settings.
Where they are allowed
Three rules, all enforced at the edge:
- The name starts with client-. The prefix is what tells the edge this is a client event rather than an attempt to forge a server publish, so it is reserved: a server-side publish using it is rejected. The browser client throws before sending if you leave it off.
- The channel is private or presence. A public channel is refused, and that is the point: the app key ships in your page, so anyone could subscribe to a public channel and start writing to it. Authorization is what makes a client trustworthy enough to broadcast, and only private and presence channels have it.
- The sender is subscribed. The connection has to already be on the channel it triggers into, so a client cannot write to a room it was never admitted to.
Break any of the three and the edge answers with a connection-level error, which is worth binding while you build:
Przykład kodu
bird.connection.bind("error", (e) => console.warn("edge refused:", e.message));Sending
Przykład kodu
import { BirdRealtime } from "@messagebird/realtime";
const bird = new BirdRealtime({
appKey: "your-app-key",
region: "us1",
authEndpoint: "/bird/auth",
});
const room = bird.subscribe("presence-room-1");
input.addEventListener("input", () => {
room.trigger("client-typing", { at: Date.now() });
});The payload is optional, and a string, an object, or an array. trigger returns true when the frame went out and false when the channel is not subscribed yet, which is the first thing most people hit: calling it in the same tick as subscribe does nothing at all. Send from a user action, as above, or from bird:subscription_succeeded if the event has to fire as soon as the client is in the room.
Receiving
Bind the name you sent, on the same channel, exactly like a server-published event:
Przykład kodu
room.bind("client-typing", (data) => showTypingIndicator(data));The sending connection is skipped, so a client never receives its own event back. Other connections belonging to the same person do receive it: somebody typing in one tab sees their own indicator appear in another, which is usually harmless and occasionally worth filtering.
Rate limits
Each connection gets 50 client events per rolling five-second window, an average of 10 per second, and the ceiling is configured on the app so every connection on it gets the same allowance. Short bursts pass; a sustained stream does not.
Going over it does not close the connection. The offending event is dropped and the edge sends back an error, so the client keeps working and quietly loses messages, which is easy to miss unless you bind the connection's error event. Throttle on the sending side: a cursor or typing indicator that fires on every keystroke or mouse move is both over the limit and faster than any UI can render.
Seeing them on your server
Client events skip your backend by default, and you can have a copy anyway. Subscribe an endpoint to the realtime.client_events group and each one arrives as a webhook whose type is realtime. plus the event name, so client-typing delivers as realtime.client-typing:
Przykład kodu
{
"data": {
"channel_name": "presence-room-1",
"event": "client-typing",
"data": "{\"at\":1785495600000}",
"connection_id": "26896.319537",
"member_id": "u_42"
},
"timestamp": "2026-07-31T09:00:00Z",
"type": "realtime.client-typing"
}member_id is present on presence channels, where the connection carries an identity, and absent on private ones. That makes the group the way to audit what your clients send each other, with one caveat about volume: subscribe it on a channel carrying cursor positions and you have signed your server up for every one of them. See Realtime webhooks for the envelope, the signature, and the other four groups.
Next steps
- Realtime webhooks covers receiving client events, and channel activity, on your own endpoint.
- Presence channels give client events a member identity, and the member list to render them against.
- Publishing events is the server-side path, for everything a client should not be trusted with.