What you'll build: a conversation two users can send messages into, with one seeing the other's messages arrive live.
Prerequisites: a Livqeno 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
npm install @ravenkash/chatnpm install @ravenkash/chat @ravenkash/reactnpm install @ravenkash/react-native @ravenkash/chatChat is optional on React Native — install it alongside
@ravenkash/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 '@ravenkash/server';
const raven = new Raven({
apiKey: process.env.RAVEN_API_KEY,
baseUrl: process.env.RAVEN_API_URL, // https://api.ravenstack.online
});
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"],
base_url=os.environ["RAVEN_API_URL"], # https://api.ravenstack.online
)
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"]])
)The mint response is a grant: it carries token, chatUrl and
apiUrl together. Return it to the browser as-is from your own endpoint:
// app/api/chat/token/route.ts — your backend, holding RAVEN_API_KEY
export async function POST() {
const grant = await raven.chat.createToken({ userId: session.userId });
return Response.json(grant); // token + chatUrl + apiUrl
}Then the browser needs no Livqeno configuration of its own — no
RAVEN_API_KEY, no gateway hostname, no API base URL:
const grant = await fetch('/api/chat/token', { method: 'POST' }).then((r) => r.json());
const chat = createChatClient(grant); // forwarded whole, unmodified
await chat.connect();That is the whole reason the addresses ride inside the grant: the same frontend code works against your laptop, hosted Livqeno, or your own cluster, and nothing in it knows which.
4. Connect from the client
import { createChatClient } from '@ravenkash/chat';
const chat = createChatClient({ token: token.token, apiUrl: token.apiUrl });
await chat.connect({ room: conversation.publicId });'use client';
import { RavenChat, useChatConnectionState } from '@ravenkash/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 '@ravenkash/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 '@ravenkash/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 '@ravenkash/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 Livqeno handles vs. what you handle
Livqeno 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. |
ORIGIN_NOT_ALLOWED, socket closed with code 4403 | The page's origin is not on this project's allow-list. The token was fine; the page holding it was not expected. | Add the origin under Project Settings → Security → Allowed Origins. Retrying will not help. Loopback origins are allowed by default. See Browser security. |
403 on a Chat REST call, code RAVEN_PERMISSION_DENIED | Same cause, on the REST surface rather than the socket. | Same fix. Note this is a real 403 you can read, not an opaque browser CORS failure — that is deliberate. |
Production notes
- Never call
raven.chat.createConversation()/createToken()(or any@ravenkash/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.