Chat
Messages that are kept. Someone joining late sees what was said, someone who was away sees what they missed, and a moderator can take a message back. That is the whole difference from Signaling, which is faster, free, and forgets everything.
Note
Your server sends it#
Sending is a REST call made with your API key, never from the client. That is deliberate: your server sees the text before we store it, so you can filter, translate, rate-limit or reject a message without us being involved and without shipping moderation rules to a client where anyone can read them.
Reading history is the exception — a client can do that with the join token it already has. See reading history from the client.
await fetch('https://api.applooma.dev/v1/chat/messages', {method: 'POST',headers: {'x-applooma-key': process.env.APPLOOMA_API_KEY,'x-applooma-secret': process.env.APPLOOMA_API_SECRET,'Content-Type': 'application/json',},body: JSON.stringify({roomName: 'lobby',sender: user.id, // your identity, not ourssenderName: user.name, // shown as it was at the timetext: 'Welcome back',}),});
Everyone currently in the channel is notified through your webhook as chat.message, so the stored copy and the live one are the same call — you do not have to send it twice.
History#
Newest first, paged by timestamp rather than by offset. A busy channel gains messages while someone is scrolling, and an offset would show them the same message twice.
const first = await get('/v1/chat/messages?roomName=lobby&limit=50');// first.nextBefore is present while there is more to fetchif (first.nextBefore) {const older = await get(`/v1/chat/messages?roomName=lobby&limit=50&before=${first.nextBefore}`);}
What a stored message carries
| Field | Notes |
|---|---|
id | Ours. Use it to recall the message later. |
sender | Your identity for the person, exactly as you sent it. |
senderName | Their display name at the time of sending. A name changed later does not rewrite what the room saw. |
text | The message, or null if it was recalled. |
data | Any structured payload you attached — an attachment reference, a quoted message, a reaction. |
kind | Free-form. text by default; use your own for images, files or anything else. |
deleted | True for a recalled message. It keeps its place in the thread. |
createdAt | Our clock, not the sender's — safe to order by. |
Unread counts#
Record how far someone has read, and ask how much they have missed. Your own messages never count as unread.
// When they open the channelawait put('/v1/chat/read', { roomName: 'lobby', identity: user.id });// For a channel list badgeconst { unread } = await get('/v1/chat/unread?roomName=lobby&identity=' + user.id);
Note
Recall#
Deleting a message hides its content and keeps its place. Replies and read positions that point at it still point at something real, and the conversation does not silently reshuffle around the gap.
await del('/v1/chat/messages/' + messageId);
Your server is told through the chat.deleted webhook, so open clients can remove it from the screen.
Reading history from the client#
Loading history is the one part of chat a client may do directly, and the join token is what allows it: we signed it, it names one channel, and your server minted it for someone it had already decided may be there. Reading what was said in that channel asks nothing more.
So there is no proxy endpoint to build. Sending, recalling and read state stay server-side, because those are decisions your backend should make.
await engine.joinChannel(token, wsUrl);const { messages, nextBefore } = await engine.loadHistory({ limit: 50 });// Older stillif (nextBefore) {const older = await engine.loadHistory({ limit: 50, before: nextBefore });}
import { useChatHistory, useMessages } from '@applooma/rtc-react';function Thread({ engine }) {const { history, hasMore, loadMore } = useChatHistory(engine);const { messages, send } = useMessages(engine);// History for what came before, live messages for what arrives nowreturn (<>{hasMore && <button onClick={loadMore}>Load older</button>}{[...history, ...messages].map((m) => (<p key={m.id}>{m.text}</p>))}</>);}
Note
Retention#
Stored chat is kept for as long as your plan says, then deleted. This is not housekeeping — holding your users' messages indefinitely is a liability we would be taking on for you, so the window is enforced by a job that runs whether or not anyone remembers it exists.
| Plan | Chat history |
|---|---|
| Sandbox | 7 days |
| Launch | 30 days |
| Scale | 90 days |
| Infinite | 1 year |
Deleting a project does not extend this. Its stored messages are pruned on the same schedule, because retiring a project must not turn them into something we keep forever.
Limits#
- Text is capped at 4,000 characters; structured data at 8 KB.
- History returns at most 200 messages per request.
- There is no file storage. Upload to your own storage and send the URL.
- There is no server-side moderation — your server is the gate, which is the point of it being a server-side API.