Raven Docs

Messages

Sending, idempotency, receiving, editing, and deleting.

Sending

const message = await chat.sendMessage({ text: 'Hello everyone!' });
// { id: 'msg_3xR…', roomId: 'conv_…', senderId: 'alice',
//   text: 'Hello everyone!', createdAt: '2026-08-18T12:00:00.000Z', ... }

The promise resolves after the message is durably in Postgres. The id and createdAt are the server's, not client guesses — which is what keeps ordering consistent across every client in the room.

OptionPurpose
textThe body. Required for text messages
typetext (default), attachment. system/event are server-only
replyToA msg_... id — the reply joins that message's thread, see Threads
clientMessageIdIdempotency key — see below
attachmentIdAn att_... id from an already-completed upload
metadataYour own JSON, capped at 4 KB
roomWhich room, if not the one you connected to

The sender is never yours to choose

A browser chat token can't set senderId. Passing one isn't an error — it's silently ignored, and the message is attributed to the token's subject. Server-side, with a project API key, senderId is required and honored — that's how a backend posts on a user's behalf, and it's why an API key must never reach a browser.

Idempotency

Networks retry. Reconnects replay. Users double-click. Any of those can turn one intended message into two.

await chat.sendMessage({ text: 'Hello', clientMessageId: 'client_123' });
await chat.sendMessage({ text: 'Hello', clientMessageId: 'client_123' });
// Same message. The second call returns the original, with deduplicated: true.

@corvidhq/chat attaches one automatically if you don't, so the SDK's own retries are already safe. Supply your own when you control the retry — a job queue, a form resubmit, an offline outbox.

The guarantee is a unique constraint on (conversationId, senderId, clientMessageId) in Postgres, not a cache — a retry an hour later, or on a different gateway, still deduplicates.

Receiving

const unsubscribe = chat.on('message', (message) => {
  console.log(`${message.senderId}: ${message.text}`);
});
 
unsubscribe(); // call this in your cleanup

on() returns an unsubscribe function — chat handlers are overwhelmingly registered inside component effects where cleanup is the common case, and a mismatched off(event, handler) is the classic way to leak one. off() still exists if you prefer it.

You receive your own messages too, deliberately: the sender renders the same canonical, server-ordered row as everyone else, instead of a local optimistic copy that has to be reconciled when the real one arrives.

Events: message, messageUpdated, messageDeleted, reactionAdded, reactionRemoved, typing, presence, read, connectionStateChanged, connected, disconnected, reconnecting, reconnected, error.

Editing and deleting

await chat.messages.update('msg_3xR…', { text: 'Updated message' });
// { ..., text: 'Updated message', edited: true, editedAt: '...' }
 
await chat.messages.delete('msg_3xR…');

React Native uses the same chat.messages.update()/chat.messages.delete() calls as web — raven.chat is a ChatClient instance, not a reimplementation.

Editing always sets editedAt and flips edited to true — history is never silently rewritten. Only the author may edit; moderators can delete, not put words in someone's mouth. Deleting is soft: the row survives with deletedAt set.

Common errors

ErrorWhyFix
senderId silently ignored (client-side send)Only a project API key can set it.Expected — see The sender is never yours to choose above.
Edit fails with a permission errorOnly the original author can edit.There's no "edit as moderator" — delete and ask them to resend, or delete it yourself.
Delete fails with a permission errorDeleting someone else's message needs chat:moderate.See Moderation.

Production notes

  • Always pass clientMessageId when you control retries (a job queue, a form resubmit) — the SDK's own automatic one only covers the SDK's own retries.
  • Don't build optimistic local message rendering — the server echo is fast enough that it isn't worth reconciling two versions of the same row.