Events from the page
Pages and apps send events to the rooms they are in, straight through WordPlus Cloud, with no request to your server: chat messages, typing, read ticks, a cursor. A room rule says which events may be sent to it, and every one is sealed like those your server sends.

What a room allows
- Node
- PHP
- Python
realtime.room( 'chat:{a}:{b}', { history: 50, emit: [ 'message', 'typing', 'reaction', 'read' ], webhook: [ 'message', 'reaction' ] },
( user, { a, b } ) => [ a, b ].includes( String( user ) ) );
$realtime->room( 'chat:{a}:{b}', fn ( $user, array $p ) => in_array( (string) $user, [ $p['a'], $p['b'] ], true ),
[ 'history' => 50, 'emit' => [ 'message', 'typing', 'reaction', 'read' ], 'webhook' => [ 'message', 'reaction' ] ] );
realtime.room("chat:{a}:{b}", lambda user, p: str(user) in (p["a"], p["b"]),
history=50, emit=["message", "typing", "reaction", "read"], webhook=["message", "reaction"])
emitlists the names, or'*'for any. None by default: a room your server alone sends to.webhooknames those your server gets too, to keep them (below).historykeeps the last events of the room for 30 minutes: what a page that reconnects missed comes again from it.- The answer for one user can narrow it:
{ emit: [ 'typing' ] }, or{ readOnly: true }for none. - The names kept for the room itself (
joined,join,leave,count,error) are never allowed.
Sending
A page sends to a room it has joined, and hears what the others send:
const chat = rt.join( 'chat:34:42' );
chat.emit( 'message', { text: 'Hi' } ); // sent, nothing waited for
const { id, at } = await chat.emitWithAck( 'message', { text: 'Hi' } ); // WordPlus Cloud took it: checked, kept, sent on
chat.volatile.emit( 'typing', { on: true } ); // dropped rather than queued while off line, never kept
await chat.timeout( 5000 ).emitWithAck( 'message', { text: 'Hi' } ); // an ack waited for 5 seconds at most
chat.on( 'message', ( data, meta ) => draw( data.text, meta.from ) ); // meta: { from, id, at, replayed? }
- Everyone in the room gets it, the other tabs and devices of the sender too. The tab that sent it does not.
emitWithAckresolves with{ id, at }once WordPlus Cloud has checked the event, kept it (when the room keeps history) and sent it on. It rejects with a code:not_allowed(the rule does not list it),forbidden(not in the room),rate_limited,too_large,bad_event.- An emit before the room is joined waits for it.
meta.fromis the sender user id, as your server vouched for them: a page cannot send as someone else.- Binary: an
ArrayBuffer,Uint8ArrayorBlobmay be anywhere in the data. It is sealed as binary, and counted in the event size. - Limits: 1 MB an event (kept in history up to 64 KB), 50 a second a connection, in bursts of up to 100.
Keeping the conversation
The conversation is for your app to keep, in its own database, as Better Messages keeps its own in the WordPress database. A room keeps its events for 30 minutes, for pages that reconnect. Your server gets the events a room rule names in webhook (webhooks) and saves them by event_id, the id the pages saw as meta.id:
- Node
- PHP
- Python
realtime.on( 'message', ( { room, from, data, event_id, at } ) => db.messages.upsert( { id: event_id, room, from, text: data.text, at } ) );
$realtime->on( 'message', fn ( array $e ) => save_message( $e['event_id'], $e['room'], $e['from'], $e['data']['text'], $e['at'] ) );
realtime.on("message", lambda e: save_message(e["event_id"], e["room"], e["from"], e["data"]["text"], e["at"]))
- Check there anything that matters: the events do not go through your server before they are sent on.
- A route of yours gives a page the conversation back: the newest 50 messages, or with
?before=<id>the 50 before that one, oldest first, as{ messages: [ { id, from, text, at, reactions } ], more }.atis in milliseconds, andreactionsmaps each reaction to who chose it. Only the two people in the chat may read it. - A reaction comes as
{ id, key }: the one reaction of the user to messageid, an emptykeytaking it back.
Ready-made: the chat
The Better Messages conversation on a room, with what a chat needs: your server conversation paged back as the reader scrolls up, the room filling in what came since, the day separators and unread line, the messages of a sender stacked, typing, reactions, the read ticks and the composer.
<wordplus-chat room="chat:34:42" messages="/chat/34/42/messages" placeholder="Write a message" reactions="1f44d 2764 1f602" max-length="2000"></wordplus-chat>
<wordplus-typing room="chat:34:42"></wordplus-typing> <!-- the typing line alone -->
messagesis your route, asked with the page cookies.chat.loadreplaces it, for a token or another shape:async ( before ) => ( { messages, more } ), wherebeforeisnullfor the newest page, or the oldest message shown ({ id, at }).- With neither, the chat shows what the room still has.
It sends what an app of your own would, so the two read each other:
| Event | Data | |
|---|---|---|
message | { text } | with an ack. Your server keeps it |
typing | { on } | volatile |
read | { id }, the newest message seen | volatile, for the sender ticks |
reaction | { id, key }, key empty to take it back | with an ack. Your server keeps it |
await rt.callScreen() loads it with the call screen, rt.ui() without. In React, <Chat room="…" messages="…" /> or load={ … } (React). Names and pictures come from your directory.