Messages are the core of a room. Each one has a stable id its sender assigns, so edits and deletes reference it, your UI can render optimistically, and resends never duplicate.
Send
const message = await room.messages.send({ text: 'hello' });
console.log(message.id);
send returns the message immediately with its id filled in, so you can render it without waiting for the round trip. Besides text, a message can carry metadata (structured data your app reads, like an attachment reference) and headers (the same idea, kept separate so UI concerns and app concerns don’t fight over one object).
Subscribe
const off = room.messages.subscribe((event) => {
switch (event.type) {
case 'created': add(event.message); break;
case 'updated': replace(event.message); break;
case 'deleted': replace(event.message); break;
}
});
Every event carries the message’s full current state, so handling an update or delete is always “replace what you have”, never patch arithmetic. Events arrive in order even when the network delivers frames out of order, because the client reconciles by version: a stale edit never overwrites a newer one, and duplicates are dropped.
Edit and delete
await room.messages.update(message.id, { text: 'hello (fixed the typo)' });
await room.messages.delete(message.id);
update replaces the message body. Fields you omit are cleared, so pass metadata again if the edit should keep it. delete leaves a tombstone: the message keeps its id and sender but its text becomes empty and deleted becomes true, which is what lets a UI show “message deleted” in place.
The message object
| Field | Type | Meaning |
|---|---|---|
id |
string |
Stable id, assigned by the sender |
clientId |
string |
Who sent it |
roomName |
string |
The room it belongs to |
text |
string |
The body, empty once deleted |
metadata, headers |
object |
App data carried with the message |
createdAt |
Date |
When it was sent |
updatedAt |
Date |
When it was last edited, equals createdAt until then |
deleted |
boolean |
True once deleted |
Chat message history
const page = await room.messages.history({ limit: 50 });
for (const message of page.messages) {
render(message); // Oldest first, edits and deletes already applied
}
if (page.hasMore) {
const older = await room.messages.history({ limit: 50, cursor: page.nextCursor });
}
History returns messages in their current state, not as a raw event log. A message that was edited comes back with its edited text, and a deleted one comes back as its tombstone, so rendering the backlog is the same code as rendering live events. Page backward by passing nextCursor until hasMore is false.
How far back history reaches is the room channel’s retention: chat: channels store messages at your plan’s ceiling by default, adjustable per prefix with a channel rule.
Capabilities
Sending, editing, and deleting need publish on the room’s channel, receiving needs subscribe, and history needs history. See Authentication and capabilities.