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

Which one do you want? If a message is worthless a minute later — a live comment scrolling past, a reaction, a raised hand — use Signaling. If someone would notice it missing when they come back, use Chat.

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.

Send a message
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 ours
senderName: user.name, // shown as it was at the time
text: '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.

Read a page, then the next
const first = await get('/v1/chat/messages?roomName=lobby&limit=50');
// first.nextBefore is present while there is more to fetch
if (first.nextBefore) {
const older = await get(
`/v1/chat/messages?roomName=lobby&limit=50&before=${first.nextBefore}`
);
}

What a stored message carries

FieldNotes
idOurs. Use it to recall the message later.
senderYour identity for the person, exactly as you sent it.
senderNameTheir display name at the time of sending. A name changed later does not rewrite what the room saw.
textThe message, or null if it was recalled.
dataAny structured payload you attached — an attachment reference, a quoted message, a reaction.
kindFree-form. text by default; use your own for images, files or anything else.
deletedTrue for a recalled message. It keeps its place in the thread.
createdAtOur 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.

Read state
// When they open the channel
await put('/v1/chat/read', { roomName: 'lobby', identity: user.id });
// For a channel list badge
const { unread } = await get('/v1/chat/unread?roomName=lobby&identity=' + user.id);

Note

Someone who has never opened a channel has missed all of it, so their first unread count is the whole history rather than zero.

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.

Recall
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.

Web
await engine.joinChannel(token, wsUrl);
const { messages, nextBefore } = await engine.loadHistory({ limit: 50 });
// Older still
if (nextBefore) {
const older = await engine.loadHistory({ limit: 50, before: nextBefore });
}
React
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 now
return (
<>
{hasMore && <button onClick={loadMore}>Load older</button>}
{[...history, ...messages].map((m) => (
<p key={m.id}>{m.text}</p>
))}
</>
);
}

Note

A token stays valid for six hours, so someone who has left the channel can still read its history until it expires. If that matters for your product, mint shorter tokens.

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.

PlanChat history
Sandbox7 days
Launch30 days
Scale90 days
Infinite1 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.