In this guide you'll build a small support chat. A customer, alice, and an
agent, bob, talk in a conversation called support-room-42. Each sees the
other's messages arrive live.
Chat follows the same pattern as every Livqeno product: your backend holds the API key and sets things up, and your app connects with a short-lived token. If you haven't seen that pattern yet, read What is Livqeno? first. It takes five minutes.
What you need:
- A Livqeno project and an API key. See API credentials.
- A backend (TypeScript, Python, Go, Java or PHP) that knows which user is logged in.
- An app: web, React, React Native, or Flutter.
The plan:
| Step | Where it runs | What happens |
|---|---|---|
| 1. Install | Both | Add the SDKs. |
| 2. Create a conversation | Backend | Make support-room-42 and add Alice and Bob as members. |
| 3. Make a token | Backend | Make a chat token for the logged-in user. |
| 4–6. Connect, send, receive | App | Join the conversation and chat. |
| 7. Disconnect | App | Close the connection cleanly. |
1. Install
Your backend needs a server SDK: @livqeno/server (TypeScript), livqeno (Python),
or the Go, Java or PHP SDK.
See Install an SDK. Your app needs one of these:
pnpm add @livqeno/chatnpm install @livqeno/chatyarn add @livqeno/chatbun add @livqeno/chatpnpm add @livqeno/chat @livqeno/reactnpm install @livqeno/chat @livqeno/reactyarn add @livqeno/chat @livqeno/reactbun add @livqeno/chat @livqeno/reactpnpm add @livqeno/react-native @livqeno/chatnpm install @livqeno/react-native @livqeno/chatyarn add @livqeno/react-native @livqeno/chatbun add @livqeno/react-native @livqeno/chatChat is optional on React Native. Add @livqeno/chat only if your app
sends messages. See React Native SDK.
dependencies:
livqeno_chat: ^0.1.1Pure Dart, no native code. A chat-only app doesn't pull in WebRTC.
2. Create a conversation (once, from your backend)
A conversation is where messages live. Think of it as one chat thread: a support ticket, a team channel, or the chat next to a call.
Only members of a conversation can read or send in it. So you add the
people who should be able to chat when you create it. Here, Alice is the
ADMIN and Bob is a normal member.
On your backend:
import { Livqeno } from '@livqeno/server';
const livqeno = new Livqeno({
apiKey: process.env.LIVQENO_API_KEY,
baseUrl: process.env.LIVQENO_API_URL, // https://api.livqeno.com
});
const conversation = await livqeno.chat.createConversation({
name: 'support-room-42',
getOrCreate: true, // safe to re-run: returns the existing one (members apply only on first create)
members: [{ userId: 'alice', role: 'ADMIN' }, { userId: 'bob' }],
});import os
from livqeno import Livqeno, CreateConversationParams, ConversationMember
livqeno = Livqeno(
api_key=os.environ["LIVQENO_API_KEY"],
base_url=os.environ["LIVQENO_API_URL"], # https://api.livqeno.com
)
conversation = livqeno.chat.create_conversation(
CreateConversationParams(
name="support-room-42",
get_or_create=True, # livqeno 0.2.0+
members=[
ConversationMember(user_id="alice", role="ADMIN"),
ConversationMember(user_id="bob"),
],
)
)You get back the conversation. Its publicId (like conv_9WcQ…) is
Livqeno's id for it. You can also refer to it by its name.
Your backend may run this code more than once, for example every time a
user opens the chat. Without getOrCreate: true (Python:
get_or_create=True), a second create with the same name fails with
409. With it, you get the existing conversation back. Its members are
not applied again, so add a new user with
addMember instead. More in
Conversations.
3. Authenticate — mint a token per user
Your app can't use the API key, so your backend gives it a chat token instead. A chat token says "this is Alice, and she may chat in these conversations, until this time". It expires after 1 hour by default.
On your backend:
const token = await livqeno.chat.createToken({
userId: 'alice',
conversations: [conversation.publicId],
});from livqeno import CreateChatTokenParams
token = livqeno.chat.create_token(
CreateChatTokenParams(user_id="alice", conversations=[conversation["publicId"]])
)The response is called a grant. It holds token (the token itself),
chatUrl (where to connect), and apiUrl (where to load history). Send it
to your app unchanged, from your own endpoint.
In a real app, userId comes from your login session, not a hard-coded
string. Here's what that endpoint looks like on your backend:
// app/api/chat/token/route.ts — your backend, holding LIVQENO_API_KEY
export async function POST() {
const grant = await livqeno.chat.createToken({ userId: session.userId });
return Response.json(grant); // token + chatUrl + apiUrl
}Leaving out conversations lets the token reach every conversation the user
is a member of.
Your app then fetches the grant and passes it straight to the SDK. It needs no Livqeno settings of its own. No API key, no URLs:
const grant = await fetch('/api/chat/token', { method: 'POST' }).then((r) => r.json());
const chat = createChatClient(grant); // forwarded whole, unmodified
await chat.connect();Because the addresses come inside the grant, the same app code works in development, staging, and production.
4. Connect from the client
Connecting opens one live connection (a WebSocket) to Livqeno and joins the
conversation you name. Once connect() finishes, you can send.
Pass the conversation you want with room. The first one you join becomes
the default for sendMessage(). A client that joins no conversation receives
nothing, and sending fails with NOT_IN_ROOM.
In your app:
import { createChatClient } from '@livqeno/chat';
const chat = createChatClient(token); // the whole grant from step 3
await chat.connect({ room: conversation.publicId });'use client';
import { LivqenoChat, useChatConnectionState } from '@livqeno/react';
function ChatPanel({ chatToken, apiUrl, room }) {
return (
<LivqenoChat token={chatToken} apiUrl={apiUrl} room={room}>
<Thread />
</LivqenoChat>
);
}
function Thread() {
const state = useChatConnectionState(); // 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'failed'
return <p>Status: {state}</p>;
}<LivqenoChat>connects when it mounts and disconnects when it unmounts. You don't callconnect().- It works like
<LivqenoRoom>for calls. You can put one inside the other for a call with a chat panel.
import { Livqeno } from '@livqeno/react-native';
const livqeno = new Livqeno({ chatToken: token.token, chatApiUrl: token.apiUrl });
await livqeno.chat!.connect('support-room-42');livqeno.chatexists only if you pass achatToken.- Add
token/endpointtoo for calls plus chat. Leave them out for a chat-only app. connect(room)takes a plain string, not{ room }.
import 'package:livqeno_chat/livqeno_chat.dart';
final chat = LivqenoChat(token: token.token, apiUrl: token.apiUrl);
await chat.connect('support-room-42');Notice that some examples use the publicId and some use the name
support-room-42. Both work. See
Conversation references.
5. Send a message
Sending is one call. It finishes only after Livqeno has saved the message, so a finished call means the message is safe.
In your app:
await chat.sendMessage({ text: 'Hello everyone!' });'use client';
import { useMessages } from '@livqeno/react';
function Composer() {
const { send } = useMessages();
return (
<input onKeyDown={(e) => e.key === 'Enter' && send(e.currentTarget.value)} />
);
}await livqeno.chat!.send('Hello everyone!');send(text) is a shortcut for sendMessage({ text }). Everything else
(history, reactions, presence) is on livqeno.chat too.
await chat.send('Hello everyone!');You don't set who sent it. The sender is always the user in the token, so Alice can't post as Bob.
6. Receive messages
New messages arrive as events. Listen once, then add each message to your UI.
In your app:
chat.on('message', (message) => console.log(message.senderId, message.text));'use client';
import { useMessages } from '@livqeno/react';
function MessageList() {
const { messages } = useMessages(); // oldest-first — render order
return messages.map((m) => <p key={m.id}>{m.senderId}: {m.text}</p>);
}livqeno.chat!.on('message', (message) => console.log(message.senderId, message.text));chat.messages.listen((message) => print('${message.senderId}: ${message.text}'));This is a stream, not an event emitter. See Flutter SDK.
You get your own messages back too. Show those, instead of a local copy, so everyone sees the same messages in the same order.
7. Disconnect
Disconnect when the user leaves the chat screen. Others then see them go offline right away, instead of after a timeout.
await chat.disconnect();Unmount <LivqenoChat>. It disconnects for you.
await livqeno.leave(); // leaves any RTC room, keeps chat connected
await livqeno.dispose(); // tears down everything, including chatchat.dispose();Done! Open the app as Bob (make a token with userId: 'bob'), and the
two of you can chat.
What happens behind the scenes
When Alice sends "Hello everyone!":
- Her app sends the message over its connection.
- Livqeno saves it and gives it an id and a time.
- Her
sendMessage()call finishes. - Livqeno delivers it to every connected member, including Alice, as a
messageevent. - If Bob is offline, nothing is lost. He sees it in history when he comes back.
If Alice's connection drops, the SDK reconnects and catches up on missed messages for you.
Who does what
| Livqeno handles | You handle |
|---|---|
| The WebSocket connection | Making tokens on your backend, for the logged-in user |
| Reconnecting and catching up on missed messages | Deciding who is a member of each conversation |
| Message order and saving | The UI: messages, typing, presence |
| Safe retries (no duplicates) |
Common errors
| Error | Cause | Fix |
|---|---|---|
409 when creating the conversation | It already exists with that name. | Pass getOrCreate: true. |
404 / ROOM_NOT_FOUND on connect or first send | The user isn't a member, or the conversation has no members. | Add them with addMember before making their token. Check pendingMembership. |
NOT_IN_ROOM on send | You didn't join a conversation in connect(), and didn't pass room. | Pass { room } to connect(). |
chat:send scope missing | The token or the user's role doesn't allow sending. | Check the role. See Members. |
| Message never reaches other users | The server refused it. Refusals don't come back as message events. | Listen for error too. See Troubleshooting. |
senderId is ignored | An app with a chat token can't set it. | This is expected. See Messages. |
ORIGIN_NOT_ALLOWED, socket closes with 4403 | Your site's address isn't on the project's allowed list. The token is fine. | Add it under Project Settings → Security → Allowed Origins. Retrying won't help. localhost is allowed by default. See Browser security. |
403 with RAVEN_PERMISSION_DENIED on a Chat REST call | Same cause, on the REST API. | Same fix. You get a real 403 you can read, not a hidden CORS error. |
Production tips
- Never call
livqeno.chat.createConversation()/createToken()(or any@livqeno/server/livqenomethod) from a browser or app. - Take
userIdfrom your own login session. A token for the wrong user lets them send as someone else. - The SDK adds a
clientMessageIdfor you, so its own retries are safe. Pass your own only when you retry (a job queue, an offline outbox).
Next steps
Your chat works. Now make it feel like a real chat app:
- Message history: show past messages when the chat opens, with a "Load earlier" button.
- Members: add and remove people, and give moderators their role.
- Typing, Presence, and Read receipts: show "Bob is typing…", who's online, and "Seen".
- Need a video call too? See RTC. Chat and RTC work independently,
and you can link them with a
roomId.