What you'll build: a conversation two users can send messages into, with one seeing the other's messages arrive live.
Prerequisites: a Raven project and a project API key (see API Keys) — conversations are created and chat tokens are minted with it, server-side, and never in a browser or app.
1. Install
Not published to npm yet. The commands below are what installation will look like once these packages are released. Until then, install from a local checkout — see Installing from source.
npm install @corvidhq/chatnpm install @corvidhq/chat @corvidhq/reactnpm install @corvidhq/react-native @corvidhq/chatChat is optional on React Native — install it alongside
@corvidhq/react-native only if your app sends messages. See
React Native SDK.
dependencies:
raven_chat:
path: ../path/to/your-checkout/sdks/flutter/raven_chatPure Dart, no native code — a messaging-only app never pulls in a WebRTC stack.
2. Create a conversation (once, from your backend)
import { Raven } from '@corvidhq/server';
const raven = new Raven({ apiKey: process.env.RAVEN_API_KEY });
const conversation = await raven.chat.createConversation({
name: 'support-room-42',
members: [{ userId: 'alice', role: 'ADMIN' }, { userId: 'bob' }],
});from raven import Raven, CreateConversationParams
raven = Raven(api_key=os.environ["RAVEN_API_KEY"])
conversation = raven.chat.create_conversation(CreateConversationParams(name="support-room-42"))3. Authenticate — mint a token per user
const token = await raven.chat.createToken({
userId: 'alice',
conversations: [conversation.publicId],
});from raven import CreateChatTokenParams
token = raven.chat.create_token(
CreateChatTokenParams(user_id="alice", conversations=[conversation["publicId"]])
)4. Connect from the client
import { createChatClient } from '@corvidhq/chat';
const chat = createChatClient({ token: token.token, apiUrl: token.apiUrl });
await chat.connect({ room: conversation.publicId });'use client';
import { RavenChat, useChatConnectionState } from '@corvidhq/react';
function ChatPanel({ chatToken, apiUrl, room }) {
return (
<RavenChat token={chatToken} apiUrl={apiUrl} room={room}>
<Thread />
</RavenChat>
);
}
function Thread() {
const state = useChatConnectionState(); // 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'failed'
return <p>Status: {state}</p>;
}<RavenChat> connects on mount and disconnects on unmount — no
separate connect() call to make yourself. It's the chat-side
equivalent of <RavenRoom>, and nests with it for a call with a chat panel.
import { Raven } from '@corvidhq/react-native';
const raven = new Raven({ chatToken: token.token, chatApiUrl: token.apiUrl });
await raven.chat!.connect('support-room-42');raven.chat is present only when a chatToken was supplied — pair it
with token/endpoint for calls-plus-chat, or omit those for a
messaging-only app. connect(room) takes a plain string, not
{ room } — the client already exists on raven, so the only
remaining question is which room.
import 'package:raven_chat/raven_chat.dart';
final chat = RavenChat(token: token.token, apiUrl: token.apiUrl);
await chat.connect('support-room-42');5. Send a message
await chat.sendMessage({ text: 'Hello everyone!' });'use client';
import { useMessages } from '@corvidhq/react';
function Composer() {
const { send } = useMessages();
return (
<input onKeyDown={(e) => e.key === 'Enter' && send(e.currentTarget.value)} />
);
}await raven.chat!.send('Hello everyone!');Convenience for sendMessage({ text }) — the common case on a phone.
Everything else on ChatClient (history, reactions, presence) is
available on raven.chat unchanged.
await chat.send('Hello everyone!');6. Receive messages
chat.on('message', (message) => console.log(message.senderId, message.text));'use client';
import { useMessages } from '@corvidhq/react';
function MessageList() {
const { messages } = useMessages(); // oldest-first — render order
return messages.map((m) => <p key={m.id}>{m.senderId}: {m.text}</p>);
}raven.chat!.on('message', (message) => console.log(message.senderId, message.text));chat.messages.listen((message) => print('${message.senderId}: ${message.text}'));A stream, not an event emitter — see Flutter SDK for why.
You receive your own messages back too — render the same server-ordered row everyone else does, rather than an optimistic local copy.
7. Disconnect
await chat.disconnect();Unmount <RavenChat> — it disconnects for you.
await raven.leave(); // leaves any RTC room, keeps chat connected
await raven.dispose(); // tears down everything, including chatchat.dispose();What Raven handles vs. what you handle
Raven handles: the WebSocket connection, reconnection with backoff and catch-up, message ordering and durability, and idempotent retries.
You handle: minting tokens from your own authenticated backend session, and the UI around messages/typing/presence.
Common errors
| Error | Why | Fix |
|---|---|---|
chat:send scope missing | Token was minted without it, or the role doesn't grant it. | Check the member's role — see Members. |
| Message never arrives for other users | Rejected server-side; a rejection never round-trips as a message event. | Listen for error too, not just message — see Troubleshooting. |
senderId in the request is ignored | A browser chat token can't set it. | Expected — see Messages. |
Production notes
- Never call
raven.chat.createConversation()/createToken()(or any@corvidhq/server/raven-sdkmethod) from a browser or app. - Derive
userIdfrom your own authenticated session — a chat token minted for the wrong user lets them send as someone else. clientMessageIdis attached automatically if you don't supply one, so retries from the SDK itself are already safe. Supply your own only when you control the retry (a job queue, an offline outbox).
Related
- Chat → Overview — the authorization model behind this.
- Messages — idempotency, editing, deleting.
- Presence, Typing Indicators, Reactions.
- Need a call alongside the conversation? See RTC — the two planes are independent.