@komozu/core (JavaScript)
Framework-agnostic browser client for Komozu, the chat for the signed-in users of your website
or app: real-time messages (Server-Sent Events), optimistic sending, typing indicators, images and videos. No UI: you draw the chat yourself. For a ready-made UI, use
@komozu/react or the embed script.
npm install @komozu/coreQuick start
Section titled “Quick start”import { KomozuClient } from '@komozu/core';
const client = new KomozuClient({ apiUrl: 'https://api.komozu.com', clientId: 'kz_live_…', // publishable key (API keys page of the dashboard) memberToken: user?.komozuToken, // the signed-in member's token, stored by your server});
await client.connect('u15'); // channel slug (or UUID)client.subscribe((state) => render(state.messages));await client.sendMessage('Hello!');@komozu/core has no UI: you draw the chat yourself. client.lookupChannel('general') (or
lookupChannel({ externalId: 'evt_1102' }) for a channel created by your backend) returns the channel, whether it
accepts images and videos (channel.attachmentsEnabled) and the project’s default colour. To read an integration’s
settings anyway (layout, colors, channel), call client.getIntegration('int_…').
Members
Section titled “Members”Only members read and write: your signed-in users. Your server creates each of them once (@komozu/node,
members.upsert) and gets a token that doesn’t expire: it stores it with the user
(Identify your users). Hand that token to the client:
| Option | Behaviour |
|---|---|
memberToken | The stored token of the signed-in user. Without it, connect() loads nothing and makes no request: state.restriction.code is MEMBER_TOKEN_REQUIRED (show a prompt to sign in). Sending, typing and uploads fail at once with the same code. |
client.setMemberToken(token | null) | A user signs in or out without reloading the page: the current channel is loaded again (null = no member, MEMBER_TOKEN_REQUIRED). |
tokenProvider: () => Promise<string | null> | Optional, wins over memberToken: called before the first connection, 60 s before exp if the token has one, and once more if the API refuses the token. client.refreshUser() calls it again. |
- The member’s profile comes from
GET /v1/members/me(state.memberDisplayName,state.memberAvatarUrl), never from the token. Onlysubis read from the token, to align your own bubbles. - A refused token is retried once with a fresh one, then surfaces as
state.restriction(TOKEN_EXPIRED,TOKEN_REVOKED,INVALID_MEMBER_TOKEN,MEMBER_NOT_FOUND). - Nothing identifying is stored in the browser: the token stays in memory, only the light / dark choice is kept per project.
Channel access and bans
Section titled “Channel access and bans”Channels are members (any member of the project) or private (only the members added to the channel). When
something blocks the reader, state.restriction carries a machine-readable code:
| Code | Meaning |
|---|---|
MEMBER_TOKEN_REQUIRED | No member token: nothing is readable |
CHANNEL_ACCESS_DENIED | Private channel, the member isn’t in it |
MEMBER_BANNED | The member is banned from the project: nothing is readable |
DOMAIN_NOT_ALLOWED | The page isn’t on the project’s allowed domains |
INTEGRATION_NOT_FOUND | Unknown or disabled integration (getIntegration) |
TOKEN_EXPIRED / TOKEN_REVOKED / INVALID_MEMBER_TOKEN | The member token was refused: get a new one from your backend |
MEMBER_NOT_FOUND | The token is valid but the member was never created (members.upsert) |
Sending a file to a channel where images and videos are turned off fails with 403 ATTACHMENTS_DISABLED. On the
Free plan, a video fails with 403 VIDEOS_NOT_ALLOWED (images only).
Real-time events: message.deleted removes the message from state.messages; message.updated replaces it (text
edited by its author, editedAt set); member.updated gives the new name and avatar to every message of that member
(and to state.memberDisplayName / memberAvatarUrl when it is the reader); access.revoked (the server closes the
stream after a removal, a ban or an access change) makes the client reconnect once and re-check its access. Listen
with client.on('messageDeleted', …) / client.on('messageUpdated', …) / client.on('memberUpdated', …) / client.on('accessRevoked', …) if you need
to react.
Live connections
Section titled “Live connections”Each open chat holds one stream. A network (IP address) can keep 50 streams open at the same
time, all projects together. A project can have 30 open at once on the Free plan, 100 on Starter and 1,000 on Pro, plus
a 20 % margin (36, 120 and 1,200); the owner gets an email at 80 %. Beyond that, the stream is refused with 429 RATE_LIMIT_EXCEEDED and Retry-After: 30: state.connectionStatus
stays reconnecting and the client tries again after that delay.
Editing your own messages
Section titled “Editing your own messages”client.editMessage(messageId, content) changes the text of the reader’s own message (attachments don’t change; the
text can be emptied only if the message keeps an image or a video). client.canEditMessage(message) tells whether to
show the action: a member edits their messages, with no time limit. Anyone else’s message fails with 403 MESSAGE_NOT_EDITABLE, admins included (they delete instead). The edited message
carries editedAt and reaches every reader live (message.updated).
Pinned messages and channel admins
Section titled “Pinned messages and channel admins”state.pinnedMessages: the channel’s pinned messages, latest pinned first, loaded with the channel and kept up to date live (message.pinned/message.unpinned). Each message carriespinnedAt. To show a pinned message that is far back,client.loadUntilMessage(id)loads older pages until it is instate.messages(20 pages at most).state.channelRole: the reader’s role in the channel,admin,userornull(not loaded). It changes live when the project owner or your backend changes the role (member.role_changed, alsoclient.on('roleChanged', …)).state.poweredByRequired:truewhen the project’s plan requires “Powered by komozu” (Free),falseon Starter and Pro,nulluntil the channel is loaded.- Admin actions:
pinMessage,unpinMessage,deleteMessageandremoveChannelMember(private channel; an admin can’t remove another admin). Without the admin role they fail at once with403 CHANNEL_ADMIN_REQUIRED, without a network call; the API checks every action anyway. - Reporting:
client.reportMessage(messageId, reason?)(any member;reasonisspam,harassment,inappropriateorother). Messages from admins and from the project owner can be reported; reporting your own message fails with400 MESSAGE_NOT_REPORTABLE; reporting the same message twice within 24 hours counts once. 10 reports per 10 minutes per IP (429). - Every message carries
authorType(member, orownerfor the project owner posting from the dashboard) andauthorRole(adminshows the Admin badge, the project owner included).
new KomozuClient({ apiUrl, clientId, memberToken?, tokenProvider?, storage?, fetch? });
client.connect(channel): Promise<void>; // slug or UUIDclient.disconnect(): void;client.destroy(): void;client.sendMessage(content, attachments?): Promise<MessageDTO>; // optimistic (state.pendingMessages)client.retryMessage(localId) / client.discardMessage(localId);client.loadMoreMessages(limit?): Promise<boolean>;client.notifyTyping(): void; // at most one signal every 2.5 sclient.uploadAttachment(file, { onProgress }): Promise<AttachmentDTO>; // images and videos, 10 MB maxclient.lookupChannel(slugOrUuid | { externalId }): Promise<{ channel; project: { primaryColor; showPoweredBy }; viewer: { role } }>;client.getIntegration(publicId): Promise<PublicIntegrationDTO>;client.listChannels(): Promise<ChannelSummaryDTO[]>; // channels the reader can openclient.setMemberToken(token | null) / client.refreshUser();client.setColorModePreference('light' | 'dark' | null);client.subscribe((state) => …): () => void;client.on('message' | 'messageDeleted' | 'messageUpdated' | 'messagePinned' | 'messageUnpinned' | 'roleChanged' | 'memberUpdated' | 'accessRevoked' | 'statusChange' | 'messagesChange' | 'stateChange' | 'error', …): () => void;client.isOwnMessage(message): boolean;client.canEditMessage(message): boolean; // own messageclient.editMessage(messageId, content): Promise<MessageDTO>;
client.loadPinnedMessages(): Promise<MessageDTO[]>; // state.pinnedMessages, latest pinned firstclient.loadUntilMessage(messageId, maxPages = 20): Promise<boolean>;client.isChannelAdmin(): boolean; // state.channelRole === 'admin'client.pinMessage(messageId) / client.unpinMessage(messageId): Promise<MessageDTO>; // adminsclient.deleteMessage(messageId): Promise<void>; // adminsclient.removeChannelMember(externalId): Promise<void>; // admins, private channelclient.reportMessage(messageId, reason?): Promise<void>; // any memberErrors returned by the API are JSON { code, error, message } with a stable code (ErrorCode of @komozu/dto).
A 429 (RATE_LIMIT_EXCEEDED) carries retryAfter (seconds).
Guides
Section titled “Guides”- getting started, identify users, advanced embed
- Server side:
@komozu/node