Skip to content

@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 an Origin header (403 SECRET_KEY_IN_BROWSER).

Node.js 20 or later, ESM, native fetch, no JWT dependency (tokens are signed with node:crypto).

Terminal window
npm install @komozu/node
import { 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' })
  1. When a user signs up, your server creates the member and gets their token: const { member, token } = await komozu.members.upsert({ id, name, avatarUrl }). id is the user id in your own database. The token doesn’t expire: store it with the user.
  2. Your server writes the stored token in the page: data-komozu-token="…" on the embed script, the token prop of KomozuChat (@komozu/react), or memberToken in @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 user
export 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 visitor
createServer(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.

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 it

Then 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).

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, idempotent
await komozu.channels.removeMembers(team.slug, ['user_43']); // their open streams are closed by the server
const 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).

// 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 first

Open chats apply a role change right away. Pin and unpin are idempotent.

// Delete a message: it disappears live for everyone, its files are purged
await komozu.messages.delete(messageId);
// Ban a member from the whole project (idempotent); their open streams are closed
await 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 deleteMessages
await 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 first
const reports = await komozu.reports.list(); // [{ message, channel, reportCount, reasons, firstReportedAt, lastReportedAt }]
await komozu.messages.delete(reports[0].message.id); // deletes the message and its reports
await komozu.reports.dismiss(messageId); // keeps the message, closes its reports
komozu.members.upsert({ id, name, avatarUrl? }): Promise<{ member: MemberAdminDTO; token: string }>;
komozu.members.revokeTokens(id): Promise<string>; // the new token
komozu.members.createToken(id, { expiresIn? }): string; // synchronous, local
komozu.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 + role
komozu.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 closed

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
}
}
CodeStatusWhen
INVALID_SECRET_KEY401Missing or unknown secret key
SECRET_KEY_IN_BROWSER403The request carried an Origin header
RATE_LIMIT_EXCEEDED429More than 600 requests per minute for the project (Retry-After)
VALIDATION_ERROR400Invalid input (for example an avatar that isn’t an https:// URL)
MEMBER_NOT_FOUND404Unknown member (addMembers lists every unknown id and changes nothing)
CHANNEL_NOT_FOUND404Unknown channel in this project
CHANNEL_EXTERNAL_ID_TAKEN409Another channel already uses this externalId
CHANNEL_LIMIT_EXCEEDED403Channel quota of the plan reached
MESSAGE_NOT_FOUND404Unknown message in this project
PIN_LIMIT_EXCEEDED409The channel already has 50 pinned messages

Full HTTP reference: server api.