@komozu/node (server)
Server SDK for Komozu, the chat for the signed-in users of your website or app.
Use it from your backend only to:
- identify your signed-in users in the chat (one token per member, created once and stored);
- create, update and delete members;
- create channels for your groups or events, and manage the members of private channels;
- moderate: delete and pin messages, ban members, review the messages reported from the chat.
Your secret key (
kz_sec_…) never goes to a browser or a mobile app. The API refuses any server call that carries anOriginheader (403 SECRET_KEY_IN_BROWSER).
Node.js 20 or later, ESM, native fetch, no JWT dependency (tokens are signed with node:crypto).
Install
Section titled “Install”npm install @komozu/nodeimport { Komozu, KomozuApiError } from '@komozu/node';
const komozu = new Komozu({ secretKey: process.env.KOMOZU_SECRET_KEY });// Self-hosted or local API: new Komozu({ secretKey, apiUrl: 'http://localhost:4000' })Identify your users
Section titled “Identify your users”- When a user signs up, your server creates the member and gets their token:
const { member, token } = await komozu.members.upsert({ id, name, avatarUrl }).idis the user id in your own database. The token doesn’t expire: store it with the user. - Your server writes the stored token in the page:
data-komozu-token="…"on the embed script, thetokenprop ofKomozuChat(@komozu/react), ormemberTokenin@komozu/core. Empty for a visitor who isn’t signed in: the chat asks them to sign in, without showing any message.
A token is required to read or write: there is no anonymous or “id only” mode. A valid token for a member that was never created
is refused (401 MEMBER_NOT_FOUND). The display name and avatar always come from upsert / update, never from the
token.
import { createServer } from 'node:http';import { Komozu } from '@komozu/node';
const komozu = new Komozu({ secretKey: process.env.KOMOZU_SECRET_KEY });
// 1. When a user signs up: create the member once, store the token with the userexport async function onSignUp(user) { const { token } = await komozu.members.upsert({ id: user.id, name: user.name, avatarUrl: user.avatarUrl ?? null }); await saveUser({ ...user, komozuToken: token });}
// When the name or avatar changes (the token stays the same)export async function onProfileChange(user) { await komozu.members.update(user.id, { name: user.name, avatarUrl: user.avatarUrl ?? null });}
// 2. Every page: the signed-in user's token, empty for a visitorcreateServer(async (req, res) => { const user = await getSignedInUser(req); // your own session res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.end(`<h1>Club news</h1><script src="https://embed.komozu.com/komozu-embed.js" data-komozu-integration="int_…" data-komozu-token="${user?.komozuToken ?? ''}" defer></script>`);}).listen(3000);Revoke a leaked token: const token = await komozu.members.revokeTokens(id) returns a new token to store. Every
token issued before is refused from then on (401 TOKEN_REVOKED) and the member’s open chats are disconnected.
Sign a token locally (advanced, no network call): komozu.members.createToken(id) signs the same kind of token
with your secret key (HS256, sub and iat). Pass { expiresIn: seconds } to add an expiry. The member must
already exist.
One channel per event
Section titled “One channel per event”A channel is one discussion. Create them from your backend, with your own id as externalId so you never have to
store the generated slug:
const channel = await komozu.channels.create({ name: 'Tournament – Nov 2', externalId: `evt_${event.id}`, // optional, unique in the project, immutable attachmentsEnabled: true, // optional: images and videos (default true)});channel.slug; // "tournament-nov-2" (generated from the name, -2, -3… if taken, never editable)
await komozu.channels.get({ externalId: `evt_${event.id}` });await komozu.channels.update({ externalId: `evt_${event.id}` }, { name: 'Tournament – Final' });await komozu.channels.delete({ externalId: `evt_${event.id}` }); // messages, integrations and files go with itThen show each event’s channel on its page with @komozu/react, by its externalId:
<KomozuChat clientId="kz_live_…" channel={{ externalId: 'evt_1102' }} />Without React, create one integration per event channel in the dashboard (an integration always shows one channel).
A ChannelRef is a slug, a channel UUID or { externalId } (sent as ?by=externalId: an external id is never
guessed from a slug).
Private group channels
Section titled “Private group channels”const team = await komozu.channels.create({ name: 'U15 team', access: 'private' });
await komozu.channels.addMembers(team.slug, ['user_42', 'user_43']); // all or nothing on unknown ids, idempotentawait komozu.channels.removeMembers(team.slug, ['user_43']); // their open streams are closed by the serverconst members = await komozu.channels.listMembers(team.slug); // each with its role ('user' | 'admin')Access levels: members (default, any member of the project) or private (only the members you add).
Channel admins and pinned messages
Section titled “Channel admins and pinned messages”// A member becomes admin of one channel: in the chat they delete, pin and remove members of a private channel.// On a members channel the role only adds rights, never access.await komozu.channels.setRole({ externalId: 'evt_1102' }, 'user_42', 'admin');await komozu.channels.setRole({ externalId: 'evt_1102' }, 'user_42', 'user');const members = await komozu.channels.listMembers('general'); // [{ externalId, displayName, …, role }]
// Pinned messages: the latest one shows at the top of the chat, the list opens from it (50 per channel at most)await komozu.messages.pin(messageId);await komozu.messages.unpin(messageId);const pinned = await komozu.messages.listPinned({ externalId: 'evt_1102' }); // latest pinned firstOpen chats apply a role change right away. Pin and unpin are idempotent.
Moderation
Section titled “Moderation”// Delete a message: it disappears live for everyone, its files are purgedawait komozu.messages.delete(messageId);
// Ban a member from the whole project (idempotent); their open streams are closedawait komozu.members.ban('user_42', { reason: 'Spam' });await komozu.members.unban('user_42');
// Delete a member: messages are kept and shown as "Deleted member", or deleted with deleteMessagesawait komozu.members.delete('user_42', { deleteMessages: true });
// Read a channel history (any access level)const { messages, nextCursor } = await komozu.messages.list({ externalId: 'evt_1102' }, { limit: 50 });
// Messages reported from the chat (Report button), grouped by message, most recently reported firstconst reports = await komozu.reports.list(); // [{ message, channel, reportCount, reasons, firstReportedAt, lastReportedAt }]await komozu.messages.delete(reports[0].message.id); // deletes the message and its reportsawait komozu.reports.dismiss(messageId); // keeps the message, closes its reportsReference
Section titled “Reference”komozu.members.upsert({ id, name, avatarUrl? }): Promise<{ member: MemberAdminDTO; token: string }>;komozu.members.revokeTokens(id): Promise<string>; // the new tokenkomozu.members.createToken(id, { expiresIn? }): string; // synchronous, localkomozu.members.update(id, { name?, avatarUrl? }): Promise<MemberAdminDTO>;komozu.members.get(id): Promise<MemberAdminDTO>;komozu.members.list({ limit?, cursor?, search? }): Promise<{ members; nextCursor }>;komozu.members.delete(id, { deleteMessages? }): Promise<void>;komozu.members.ban(id, { reason? }): Promise<MemberBanDTO>;komozu.members.unban(id): Promise<void>;
komozu.channels.create({ name, access?, externalId?, attachmentsEnabled? }): Promise<ChannelDTO>;komozu.channels.get(ref): Promise<ChannelDTO>;komozu.channels.list(): Promise<ChannelDTO[]>;komozu.channels.update(ref, { name?, access?, attachmentsEnabled? }): Promise<ChannelDTO>;komozu.channels.delete(ref): Promise<void>;komozu.channels.addMembers(ref, memberIds): Promise<void>;komozu.channels.removeMembers(ref, memberIds): Promise<void>;komozu.channels.listMembers(ref): Promise<ChannelMemberDTO[]>; // MemberAdminDTO + rolekomozu.channels.setRole(ref, memberId, 'admin' | 'user'): Promise<ChannelMemberDTO>;
komozu.messages.list(ref, { limit?, cursor? }): Promise<{ messages; nextCursor }>;komozu.messages.delete(messageId): Promise<void>;komozu.messages.pin(messageId): Promise<MessageDTO>;komozu.messages.unpin(messageId): Promise<MessageDTO>;komozu.messages.listPinned(ref): Promise<MessageDTO[]>;
komozu.reports.list(): Promise<MessageReportDTO[]>;komozu.reports.dismiss(messageId): Promise<number>; // number of reports closedErrors
Section titled “Errors”Every failed call throws a KomozuApiError with a stable machine-readable code, the HTTP status and an English
message:
try { await komozu.channels.create({ name: 'Finals', externalId: 'evt_9' });} catch (error) { if (error instanceof KomozuApiError && error.code === 'CHANNEL_EXTERNAL_ID_TAKEN') { // already created }}| Code | Status | When |
|---|---|---|
INVALID_SECRET_KEY | 401 | Missing or unknown secret key |
SECRET_KEY_IN_BROWSER | 403 | The request carried an Origin header |
RATE_LIMIT_EXCEEDED | 429 | More than 600 requests per minute for the project (Retry-After) |
VALIDATION_ERROR | 400 | Invalid input (for example an avatar that isn’t an https:// URL) |
MEMBER_NOT_FOUND | 404 | Unknown member (addMembers lists every unknown id and changes nothing) |
CHANNEL_NOT_FOUND | 404 | Unknown channel in this project |
CHANNEL_EXTERNAL_ID_TAKEN | 409 | Another channel already uses this externalId |
CHANNEL_LIMIT_EXCEEDED | 403 | Channel quota of the plan reached |
MESSAGE_NOT_FOUND | 404 | Unknown message in this project |
PIN_LIMIT_EXCEEDED | 409 | The channel already has 50 pinned messages |
Full HTTP reference: server api.