`/api/im/bootstrap` 是**你的产品接口**,不是 WuKongIM HTTP API。浏览器不直接调用 `/user/token` 或 `/route`;服务端凭据、路由选择与响应筛选留在 BFF。
以下三个代码段按顺序放进同一个 TS 模块。先取连接材料,再创建连接所有者,最后绑定页面。
① 获取 BFF 连接材料
```ts
interface IMBootstrap {
uid: string;
token: string;
websocketUrl: string;
}
async function fetchIMBootstrap(): Promise {
const response = await fetch('/api/im/bootstrap', {
credentials: 'include',
cache: 'no-store',
headers: { accept: 'application/json' },
});
if (!response.ok) throw new Error(`IM bootstrap failed: ${response.status}`);
return response.json() as Promise;
}
```
② 完整连接封装:监听、10 秒超时与销毁
```ts
import { WKIM, WKIMChannelType, WKIMEvent, type RecvMessage } from 'easyjssdk';
export class EasyChatClient {
private im?: WKIM;
constructor(private readonly receiveMessage: (message: RecvMessage) => void) {}
private readonly onConnect = (_result: unknown) => {
console.info('EasySDK connected');
};
private readonly onDisconnect = (_info: unknown) => {
console.info('EasySDK disconnected');
};
private readonly onMessage = (message: RecvMessage) => {
this.receiveMessage(message);
};
private readonly onError = (_error: unknown) => {
console.error('EasySDK operation failed');
};
async start(bootstrap: IMBootstrap): Promise {
this.stop();
const im = WKIM.init(
bootstrap.websocketUrl,
{ uid: bootstrap.uid, token: bootstrap.token },
{ singleton: false, debugLogging: false },
);
im.on(WKIMEvent.Connect, this.onConnect);
im.on(WKIMEvent.Disconnect, this.onDisconnect);
im.on(WKIMEvent.Message, this.onMessage);
im.on(WKIMEvent.Error, this.onError);
this.im = im;
await this.connectWithin(im, 10_000);
}
private async connectWithin(im: WKIM, timeoutMs: number): Promise {
let timeout: ReturnType | undefined;
try {
await Promise.race([
im.connect(),
new Promise((_, reject) => {
timeout = setTimeout(
() => reject(new Error(`EasySDK connect exceeded ${timeoutMs} ms`)),
timeoutMs,
);
}),
]);
} catch (error) {
im.destroy();
if (this.im === im) this.im = undefined;
throw error;
} finally {
if (timeout) clearTimeout(timeout);
}
}
async sendText(targetUid: string, text: string) {
if (!this.im?.isConnected) throw new Error('EasySDK is not connected');
return this.im.send(targetUid, WKIMChannelType.Person, {
type: 1,
version: 1,
content: text,
});
}
stop(): void {
const im = this.im;
if (!im) return;
im.off(WKIMEvent.Connect, this.onConnect);
im.off(WKIMEvent.Disconnect, this.onDisconnect);
im.off(WKIMEvent.Message, this.onMessage);
im.off(WKIMEvent.Error, this.onError);
im.destroy();
this.im = undefined;
}
}
```
`on` 和 `off` 使用同一函数引用;`singleton: false` 避免多实例或热更新互相销毁连接。完整连接等待限时 10 秒,失败后 `destroy`。应用通常只持有一个 `EasyChatClient`。
组件生命周期与诊断日志
React 可在根级 Provider 持有客户端,Effect 清理时调用 `stop()`;Vue/Svelte 在组件或应用卸载钩子里释放。SDK 的 CONNECT 请求有内部超时,但 WebSocket 打开阶段仍可能悬挂,因此需要限制完整等待。
`debugLogging: false` 是默认值。仅在受控诊断窗口开启;SDK 只输出脱敏运行元数据,应用回调与采集器也不得记录完整事件、结果或 Payload。
## 4. 收发第一条消息 [#4-收发第一条消息]
1. 两端都确认出现 **Connected**。
2. Alice 发给 Bob;发送端确认 **Message sent successfully**。
3. Bob 确认 **Message received**;官方测试日志只显示序号与频道类型。
4. Bob 回发 Alice,验证反向接收。
① 目标使用对方 UID。② Alice 的发送请求成功。③ Alice 收到 Bob 的回发。发送结果与对方接收需要分别核对。
发送 Promise 成功代表服务端返回了发送结果,**不代表对方已读或已处理**。本页只验证在线收发;离线恢复、会话和未读见 [SDK 选择](/zh/sdk)。
③ 页面按钮与接收区完整示例
两端都连接后再发送。Alice 目标为 `bob`,Bob 改为 `alice`;它们表示各自业务 UID,需与 BFF 返回值一致。在自己的 Message 回调中核对 `fromUid`、`channelId`、`messageId` 和 Payload。页面只显示最近一条消息,避免无界历史。
```html
```
```ts
const output = document.querySelector('#received-message');
const button = document.querySelector('#send-message');
if (!output || !button) throw new Error('Missing message UI');
button.disabled = true;
const chat = new EasyChatClient((message) => {
output.textContent = JSON.stringify({
fromUid: message.fromUid,
payload: message.payload,
});
});
window.addEventListener('pagehide', () => chat.stop(), { once: true });
try {
await chat.start(await fetchIMBootstrap());
button.disabled = false;
} catch {
chat.stop();
output.textContent = 'Connection failed';
}
button.onclick = async () => {
try {
// Bob sends to 'alice'. Click only after both clients are connected.
await chat.sendText('bob', 'Hello from Web EasySDK');
console.info('SEND completed');
} catch {
console.error('Send failed; check its outcome before retrying');
}
};
```
## 5. 清理连接 [#5-清理连接]
退出账号、卸载应用或离开页面时调用 `chat.stop()`:用原回调 `off`,再 `destroy()`。页面订阅者移除自己的回调,连接所有者销毁实例。
补充验证:不可达地址与刷新
用不可达地址确认 10 秒内失败并销毁,页面不会永久停在 Connecting。刷新、关闭或退出后,确认旧实例已经解绑并销毁,没有重复连接。
## 6. 常见问题 [#6-常见问题]
| 现象 | 先检查 |
| -------------------------- | ------------------------------------------------- |
| 缺少 WebSocket 或编码 API | 浏览器与构建环境;浏览器支持不等于小程序支持 |
| 连接被拒绝 | BFF 返回的 WSS 地址、Token 时效、证书、代理 Upgrade、Gateway 可达性 |
| 刷新后重复收消息 | 避免每次渲染都 `on`;卸载时 `off` / `destroy` |
| Console 出现 Token 或 Payload | 锁文件与 bundle 的实际 SDK 版本,以及应用回调、错误处理和采集器 |
日志问题用固定版本 npm 产物复现,仅保留低敏证据。SDK 默认关闭诊断,开启后也只应输出脱敏运行元数据。
## 下一步 [#下一步]
继续阅读[消息收发](/zh/guide/integration/messaging)与[上线检查](/zh/guide/integration/acceptance)。[版本与验证记录](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md#javascript)保留历史环境和范围;当前边界见 [API 版本与兼容性](/zh/api/compatibility)。
需要接入大模型?跟随 [Agent 流式回复](/zh/sdk/easy/agent-streaming)更新同一条回答,并在后端保护模型密钥。
# iOS 快速接入 (/zh/sdk/easy/ios/getting-started)
本教程使用 **1.1.1**,让 Alice 与 Bob 在两个独立客户端中在线收发。连接由 WebSocket JSON-RPC CONNECT 完成鉴权。
## 1. 准备接入 [#1-准备接入]
准备以下条件:
* iOS 部署目标使用 **iOS 15 或更高版本**。包清单声明 iOS 13,但 `v1.1.1` 的公开 `WuKongEasySDK` 类标记为 `@available(iOS 15.0, ...)`,因此本教程采用更保守的真实 API 下限;
* 使用支持 Swift 5.7 Package 的 Xcode 工具链;
* 一个 `/readyz` 正常、WebSocket Gateway 可从设备访问的 WuKongIM 单节点集群或多节点集群;
* 业务后端能为 Alice 和 Bob 分别返回 `uid`、短期 `token` 与 `websocketUrl`。
先阅读[身份与 Token](/zh/guide/integration/authentication)。不要把固定 Token 写进 App、源码仓库、日志或截图。
想先看到实际收发效果,可按[运行官方示例](/zh/sdk/easy/examples#ios)准备两端;下文说明如何接入自己的应用。
默认设备类别为 **APP `0`**;业务后端保存 Token 时使用相同的 `device_flag`。APP 为 `0`,WEB 为 `1`,PC 为 `2`。
## 2. 安装 SDK [#2-安装-sdk]
### Swift Package Manager [#swift-package-manager]
在 Xcode 中选择 **File → Add Package Dependencies**,输入:
```text
https://github.com/WuKongIM/WuKongEasySDK-iOS.git
```
Dependency Rule 选择 **Exact Version**,填写 `1.1.1`。如果项目维护 `Package.swift`,使用精确规则:
```swift
dependencies: [
.package(
url: "https://github.com/WuKongIM/WuKongEasySDK-iOS.git",
exact: "1.1.1"
)
]
```
## 3. 连接与监听 [#3-连接与监听]
把业务后端响应映射到应用模型。真实实现应通过 HTTPS 获取,不要在客户端调用 WuKongIM HTTP API 管理接口:
```swift
struct IMBootstrap: Decodable {
let uid: String
let token: String
let websocketUrl: String
}
```
`websocketUrl` 在本地开发可以是 `ws://`;生产环境应是由业务后端选择并返回的 `wss://` 地址。
下面的对象先注册监听器,再连接;退出时使用创建监听器时返回的 `EventListener` 精确移除,避免页面重进后重复处理消息。
```swift
import Combine
import Foundation
import WuKongEasySDK
@MainActor
final class EasyChatClient: ObservableObject {
@Published private(set) var isConnected = false
@Published private(set) var messages: [Message] = []
private var sdk: WuKongEasySDK?
private var listeners: [EventListener] = []
func start(with bootstrap: IMBootstrap) async throws {
stop()
let config = try WuKongConfig(
serverUrl: bootstrap.websocketUrl,
uid: bootstrap.uid,
token: bootstrap.token,
connectionTimeout: 15,
requestTimeout: 15,
maxReconnectAttempts: 5,
enableDebugLogging: false,
logLevel: .error,
enableJsonLogging: false // 生产配置同时关闭 JSON 摘要。
)
let sdk = WuKongEasySDK(config: config)
listeners.append(sdk.onConnect { [weak self] _ in
Task { @MainActor in self?.isConnected = true }
})
listeners.append(sdk.onDisconnect { [weak self] _ in
Task { @MainActor in
self?.isConnected = false
print("WuKongEasySDK disconnected")
}
})
listeners.append(sdk.onMessage { [weak self] message in
Task { @MainActor in
guard self?.messages.contains(where: { $0.messageId == message.messageId }) == false
else { return }
self?.messages.append(message)
if let self, self.messages.count > 100 {
self.messages.removeFirst(self.messages.count - 100)
}
}
})
listeners.append(sdk.onError { _ in
print("WuKongEasySDK operation failed")
})
self.sdk = sdk
do {
try await sdk.connect()
} catch {
stop() // 超时、认证或网络失败后释放 socket 与监听器。
throw error
}
}
func sendText(to uid: String, text: String) async throws -> SendResult {
guard let sdk, isConnected else { throw WuKongError.notConnected }
let payload: MessagePayload = [
"type": 1,
"version": 1,
"content": text
]
return try await sdk.send(
channelId: uid,
channelType: .person,
payload: payload
)
}
func stop() {
if let sdk {
listeners.forEach { sdk.removeListener($0) }
sdk.disconnect()
}
listeners.removeAll()
messages.removeAll()
sdk = nil
isConnected = false
}
deinit {
// 视图或应用生命周期仍应主动调用 stop()。
}
}
```
不要把 `onMessage` 的匿名闭包丢掉;`removeListener` 需要注册时返回的监听器对象。示例把连接与请求上限固定为 15 秒,失败后立即 `stop()`;自动重连最多 5 次,但 UI 仍要显示未就绪状态。SwiftUI 可以让一个上层 `@StateObject` 持有 `EasyChatClient`,在退出账号或应用根视图销毁时调用 `stop()`。
## 4. 收发第一条消息 [#4-收发第一条消息]
`productAPI.fetchIMBootstrap()` 是你的业务登录接口,`chatClient` 是上一步创建的 `EasyChatClient`。此处是接入片段;可直接运行的完整 App 见[官方示例](/zh/sdk/easy/examples#ios)。
设备标志与 Payload 已在当前服务端兼容;在用户完成产品登录后取得 Alice 的 `IMBootstrap` 并启动:
```swift
let aliceBootstrap = try await productAPI.fetchIMBootstrap()
try await chatClient.start(with: aliceBootstrap)
_ = try await chatClient.sendText(
to: "bob",
text: "Hello from iOS EasySDK"
)
print("SEND completed")
```
返回 `SendResult` 说明 Alice 获得了服务端发送结果。Bob 还必须在自己的 `onMessage` 回调中独立看到相同业务 Payload;不要仅凭本地列表插入判断对方已收到。
1. 在两台设备或两个独立 App 进程中分别登录 Alice 与 Bob;
2. 两端都等到 `onConnect` 后再启用发送按钮;
3. Alice 向个人 Channel `bob` 发送消息,保存 `messageId` 与 `messageSeq`;
4. Bob 在 `onMessage` 中核对 `fromUid == "alice"`、Channel 和 Payload;
5. Bob 向 Alice 回发,再验证反向链路;
6. 退出页面或账号,调用 `stop()`,确认没有重复监听和后台连接。
## 5. 清理连接 [#5-清理连接]
在退出账号或销毁连接所有者时调用 `chatClient.stop()`。它移除 `EventListener` 并 `disconnect()`;页面重新展示时不要重复创建应用级连接。示例只保留最近 100 条消息用于展示,不是持久消息库。
## 6. 常见问题 [#6-常见问题]
* **编译提示 API 仅 iOS 15 可用**:把部署目标提高到 iOS 15,不能只看 `Package.swift` 中较低的平台声明。
* **连接或认证失败**:从设备网络访问后端返回的地址,检查 WSS、证书与代理 Upgrade,再刷新短期 Token;不要把容器内地址返回给手机。
* **SEND 或 RECV 的 Payload 无法解析**:确认服务端包含 EasySDK JSON-RPC 兼容实现且代理没有改写消息;固定版本发送对象,服务端对对象与 Base64 做兼容并输出对象 RECV。
* **设备类型不符合预期**:确认业务代码没有覆盖默认 `.app`,并按 APP `0`、WEB `1`、PC `2` 核对线路值,不要沿用旧整数。
* **消息重复出现**:确认监听器只注册一次,并按 `messageId` 合并实时和后续同步结果。
* **发送成功但 Bob 没收到**:分别检查 Alice 的发送结果、Bob 的实时连接以及产品后续的离线同步,不要自动重发一个已经提交的消息。
## 其他安装方式:CocoaPods [#其他安装方式cocoapods]
```ruby
target 'YourApp' do
pod 'WuKongEasySDK', '1.1.1'
end
```
然后运行 `pod install`,并从 `.xcworkspace` 打开项目。两种安装方式选择一种即可。
## 下一步 [#下一步]
继续阅读[消息收发](/zh/guide/integration/messaging)与[上线检查](/zh/guide/integration/acceptance)。需要离线恢复、会话、未读或推送时,先查看 [SDK 选择](/zh/sdk)。[版本与验证记录](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md#ios)保留各次验证的完整环境和范围。
# Python 快速接入 (/zh/sdk/easy/python/getting-started)
使用 Python 3.11+、asyncio 和 PyPI **0.1.0** 完成在线收发。每个客户端属于一个事件循环。
## 1. 准备接入 [#1-准备接入]
按[认证与 Token](/zh/guide/integration/authentication)让受信业务后端分别提供两人的 `uid`、`token` 和 `websocketUrl`。客户端只连接 Gateway,不调用 WuKongIM HTTP API 管理接口。
设备类别是 APP `0`、WEB `1`、PC/Desktop `2`。**Python 默认 Desktop `2`**,后端保存 Token 时必须使用相同设备类别。开发地址通常为 `ws://127.0.0.1:5200`;仅当 listener 或代理配置了 `/ws` 时使用 `ws://127.0.0.1:5200/ws`。生产使用 `wss://`,跨机器时使用客户端实际可达地址。
想先看到实际收发效果,可按[运行官方示例](/zh/sdk/easy/examples#python)准备两端;下文说明如何接入自己的应用。
## 2. 安装 SDK [#2-安装-sdk]
本文使用 [PyPI `wukong-easy-sdk==0.1.0`](https://pypi.org/project/wukong-easy-sdk/0.1.0/),要求 Python 3.11+。分发名是 `wukong-easy-sdk`,导入名是 `wukong_easy_sdk`。
```sh
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --index-url https://pypi.org/simple "wukong-easy-sdk==0.1.0"
```
Windows PowerShell 将激活命令替换为 `.venv\Scripts\Activate.ps1`。运行依赖为 `websockets>=15.0.1,<18`,仓库 `uv.lock` 固定开发与验证依赖。
## 3. 连接与监听 [#3-连接与监听]
下面代码用环境中的身份向 `WKIM_PEER` 发送一条消息,并继续接收 10 秒;Bob 必须已经在线。实际应用应让客户端存活到应用退出。
```python
import asyncio
import os
from wukong_easy_sdk import AuthOptions, WKIM, WKIMChannelType, WKIMEvent
async def main():
im = WKIM.init(
os.environ.get("WKIM_URL", "ws://127.0.0.1:5200"),
AuthOptions(uid=os.environ["WKIM_UID"], token=os.environ["WKIM_TOKEN"]),
)
def receive(message):
# 将 message["payload"] 交给应用 UI 或有界队列。
# 不要把完整消息或 Token 写入生产日志。
print("Message received")
listener = im.on(WKIMEvent.MESSAGE, receive)
im.on(WKIMEvent.ERROR, lambda error: print("EasySDK operation failed"))
async with im:
ack = await im.send(
os.environ["WKIM_PEER"], WKIMChannelType.PERSON,
{"type": 1, "content": "你好,Python!"},
)
assert ack["reasonCode"] == 1
await asyncio.sleep(10)
im.off(WKIMEvent.MESSAGE, listener)
asyncio.run(main())
```
`async with im` 在进入时等待 CONNECT 鉴权,退出时执行 `destroy()`。群聊使用 `WKIMChannelType.GROUP`,Channel 与成员关系由业务后端预先建立。Payload 接受 JSON 对象或数组,按 UTF-8 JSON 编码为 Base64;接收兼容对象、JSON 文本和 Base64 JSON。
Python 参数使用 snake\_case,消息和结果字典保留 JS 的 camelCase:发送结果包含 `messageId`、`messageSeq`、`reasonCode`;接收还包含 `header`、`channelId`、`channelType`、`fromUid`、秒级 `timestamp` 和 `payload`。消息 ID 是字符串,序号保持完整整数精度。自定义事件通过 `WKIMEvent.CUSTOM_EVENT` 提供 `id`、`type`、毫秒级 `timestamp` 和 `data`。
`send()` 支持 `client_msg_no`、`header`、`setting`、`topic` 关键字参数;默认 `header.redDot=true`,显式 false 会保留。可选消息标记与 Channel 类型仍取决于服务端支持。
自动 RECVACK 表示消息已进入 SDK 分发队列,不代表业务处理完成或已读。发送成功、对端接收和业务处理的区别见[消息收发](/zh/guide/integration/messaging)。
## 4. 收发第一条消息 [#4-收发第一条消息]
下载与安装包一致的示例源码,继续使用当前虚拟环境:
```sh
git clone --branch v0.1.0 --depth 1 https://github.com/WuKongIM/WuKongEasySDK-Python.git
```
两个终端分别设置自己的 `WKIM_UID`、`WKIM_TOKEN`、`WKIM_PEER` 和可选的 `WKIM_URL`,运行:
```sh
python WuKongEasySDK-Python/examples/chat.py
```
例如 Alice 的 `WKIM_UID=alice`、`WKIM_PEER=bob`;Bob 相反。Token 通过各自受信环境提供。两端都显示 `Connected` 后输入消息,再反向发送,输入 `/quit` 清理退出。示例主动展示聊天内容,SDK 自身默认静默。
两端都连接成功后,Alice 向 `bob` 发送,Bob 在消息回调中核对发送者和正文;再由 Bob 向 `alice` 回发。发送结果表示服务端接受请求,不能代替对端接收或已读。
## 5. 清理连接 [#5-清理连接]
每个实例只属于一个 asyncio 事件循环,没有全局单例。同步与异步回调在独立任务中串行分发,异步回调可以 `await im.send(...)` 回复,也可以断开或销毁客户端。不要阻塞事件循环,或在回调中等待同一串行分发器上的后续事件。
| 操作 | 语义 |
| ---------------------------------------------------- | ------------------------------ |
| `await im.connect()` | 并发调用共享一次鉴权;已连接时返回当前结果 |
| `im.is_connected` | 当前连接已通过鉴权 |
| `im.on(event, callback)` / `im.off(event, callback)` | 保存并移除原回调;已经开始执行的回调可能继续完成 |
| `await im.ping()` | 等待同 ID 响应,包括有效的 `result: null` |
| `await im.disconnect()` | 取消待处理请求、socket、心跳与重连;之后可重新连接 |
| `await im.destroy()` | 永久关闭实例并释放监听器,可重复调用 |
| 更换账号、Token 或地址 | 关闭旧实例,再创建新实例 |
取消一个 `connect()` 等待者不会取消共享连接,需要停止时调用 `disconnect()`。取消或超时的发送会释放请求占用,但已到达服务端的消息仍可能提交。
## 6. 常见问题 [#6-常见问题]
`WKIMOptions` 的时间单位均为秒:连接总超时 10 秒、请求 15 秒、心跳间隔 25 秒、Pong 超时 10 秒、关闭超时 2 秒。成功鉴权后的意外断线最多重试 5 次,从 1 秒指数增长到最多 30 秒,附带 20% 抖动。首次连接失败、鉴权拒绝、服务端主动断开、协议错误、事件队列满、证书校验失败和手动退出停止自动重试。
默认上限为 1,024 个待处理请求、4 MiB 序列化待处理请求、1 MiB 单条线路消息,以及 256 条事件、4 MiB 事件线路大小预算,包含当前执行事件;Python 对象开销另计。请求满返回 `ErrorCode.QUEUE_FULL`;事件队列满关闭连接,未进入队列的消息不被确认,过载时生命周期事件为尽力投递。保持回调短小并控制应用处理速度。
SDK 不离线排队或自动重发。超时与丢失 SENDACK 可能导致提交结果未知,应保留 `client_msg_no` 通过业务后端对账,再决定是否重试。错误通过 `WKIMError.code` 保留服务端原因码或本地 `ErrorCode`,错误文本不回显敏感响应。
WSS 默认验证证书链与主机名,最低 TLS 1.2。私有 CA 使用 `WKIMOptions(ca_file="/path/ca.pem")`,交互示例读取 `WKIM_CA_FILE`。不提供跳过校验的选项;不自动读取系统代理,直接使用传入的 Gateway/代理 URL。
默认不输出日志;`WKIMOptions(debug_logging=True)` 只启用固定生命周期元数据,不记录 Token、Payload、URL、原始帧、服务端响应文本或底层异常对象。
## 可选:群聊 [#可选群聊]
群聊使用 `WKIMChannelType.GROUP`,无需客户端订阅。由可信后端建立群、添加成员并管理权限;接收继续使用 `WKIMEvent.MESSAGE`。跨节点成员变更需要包含[成员缓存修复](https://github.com/WuKongIM/WuKongIM/commit/2a295e0d9881ef5356728a85d56b052c4b0d9c86)的服务端,只升级 Python 包无法修复旧服务端行为。Slot Leader 切换时在线路由需要恢复,不能保证切换期间连续投递。完整示例与版本条件见[群聊说明](https://github.com/WuKongIM/WuKongEasySDK-Python/blob/527f37c876326e7ad3cc48c89828c4c3ffed09fc/examples/group_chat.py)。
## 下一步 [#下一步]
继续阅读[消息收发](/zh/guide/integration/messaging)与[上线检查](/zh/guide/integration/acceptance)。需要离线恢复、会话、未读或推送时,先查看 [SDK 选择](/zh/sdk)。[版本与验证记录](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md#python)保留各次验证的完整环境和范围。
# Rust 快速接入 (/zh/sdk/easy/rust/getting-started)
使用 Rust 1.86+、Tokio 和 crates.io **0.1.0** 完成在线收发。支持原生程序,不支持浏览器/WASM。
## 1. 准备接入 [#1-准备接入]
业务后端返回当前用户的 `uid`、短期 `token` 和 `websocketUrl`。按[认证与 Token](/zh/guide/integration/authentication)保护注册和轮换流程;客户端不应直接调用 WuKongIM HTTP API 管理接口。
`Auth::new` 默认设备类别是 **PC/Desktop `2`**。协议值为 APP `0`、WEB `1`、PC `2`,Token 必须按同一设备类别保存。若你的原生宿主使用 APP 类别,显式设置 `auth.device_flag = DeviceFlag::App`,并同步后端注册类别。
`Auth::new` 生成的 `device_id` 在同一个客户端重连时保持不变;需要跨进程保留设备身份时,由应用读取并设置自己的稳定设备 ID。每个身份创建一个 `Client`;`clone()` 共享同一条连接,适合在 Tokio 任务之间传递。
想先看到实际收发效果,可按[运行官方示例](/zh/sdk/easy/examples#rust)准备两端;下文说明如何接入自己的应用。
## 2. 安装 SDK [#2-安装-sdk]
官方仓库:[WuKongIM/WuKongEasySDK-Rust](https://github.com/WuKongIM/WuKongEasySDK-Rust)。已发布 [crates.io `0.1.0`](https://crates.io/crates/wukong-easy-sdk/0.1.0),需要 Rust **1.86+** 和 Tokio。使用以下精确版本:
```toml
[dependencies]
wukong-easy-sdk = "=0.1.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde_json = "1"
```
包名是 `wukong-easy-sdk`,Rust 导入名是 `wukong_easy_sdk`。提交应用的 `Cargo.lock`,让后续构建复用已解析的依赖。当前实现支持原生 TCP/TLS,WSS 使用 rustls 与 WebPKI 根证书;不支持浏览器/WASM。
## 3. 连接与监听 [#3-连接与监听]
这个完整程序先订阅事件,再连接并向 Bob 发一条消息,随后清理。把环境变量换成业务后端提供的材料。Alice 和 Bob 必须是不同 UID,且 Bob 已在线。
```rust
use serde_json::json;
use wukong_easy_sdk::{Auth, ChannelType, Client, Event, Options};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = Client::new(
std::env::var("WK_WS_URL")?,
Auth::new(std::env::var("WK_UID")?, std::env::var("WK_TOKEN")?),
Options::default(),
)?;
let mut events = client.subscribe();
let listener = tokio::spawn(async move {
loop {
match events.recv().await {
Ok(Event::Message(message)) => {
// 在应用 UI 中展示或保存 message.payload,不记录完整正文。
let _ = &message.payload;
}
Ok(Event::CustomEvent(event)) => {
// event.event_type 和 event.data 由业务协议定义。
let _ = (&event.event_type, &event.data);
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
// 已丢失事件;通知应用通过业务后端补偿状态。
break;
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
_ => {}
}
}
});
let result = async {
client.connect().await?;
let ack = client.send(
"bob", ChannelType::Person,
json!({"type": 1, "content": "你好,Rust 🦀"}),
).await?;
// ack 是服务端发送结果,不是 Bob 已读或业务处理完成。
let _ = ack;
Ok::<_, wukong_easy_sdk::Error>(())
}.await;
client.destroy().await;
listener.abort();
let _ = listener.await;
result?;
Ok(())
}
```
成功的 `send` 返回 `SendResult`,包括字符串 `message_id`、`u64` 类型的 `message_seq` 和 `reason_code`。业务错误返回 `Error::Server { code }`,保留服务端数值,不把失败响应当成功。不要将 SENDACK 等同于接收、展示或业务完成,详见[消息收发](/zh/guide/integration/messaging)。
## 4. 收发第一条消息 [#4-收发第一条消息]
要验证持续双向通信,运行仓库里的终端 example,而不是上面发送后立即退出的程序:
```bash
git clone https://github.com/WuKongIM/WuKongEasySDK-Rust.git
cd WuKongEasySDK-Rust
git checkout v0.1.0
cargo build --locked --example chat
```
由受信开发终端按[运行官方示例](/zh/sdk/easy/examples)准备两组 Token,将 Alice 与 Bob 的 `device_flag` 都设为 `2`。在两个终端分别运行:
```bash
# Alice
WK_WS_URL=ws://127.0.0.1:5200 WK_UID=alice WK_TOKEN=alice-token \
WK_PEER_UID=bob cargo run --locked --example chat
# Bob
WK_WS_URL=ws://127.0.0.1:5200 WK_UID=bob WK_TOKEN=bob-token \
WK_PEER_UID=alice cargo run --locked --example chat
```
两端都显示连接成功后再输入内容。示例会报告服务端接受发送、收到消息和连接状态,避免把消息正文写入日志;应用可以从 `Event::Message` 中读取 `from_uid`、Channel 和 Payload。输入 `/quit` 或 Ctrl-C 清理退出。
## 5. 清理连接 [#5-清理连接]
| 任务 | Rust API 与语义 |
| ----------- | ------------------------------------------------- |
| 订阅/取消订阅 | `subscribe()` 返回 Tokio broadcast 接收器;drop 接收器取消订阅 |
| 并发连接 | `connect().await` 共享当前尝试;取消一个 Future 不会取消共享连接 |
| 首次失败 | 返回错误,由应用决定是否再次连接 |
| 网络重连 | 已建立的连接异常断开后,默认最多 5 次指数退避重连,包含抖动 |
| 手动断开 | `disconnect().await` 取消鉴权、I/O 和重连;之后可再次 connect |
| 账号退出 | `destroy().await` 永久关闭所有 clone,并结束订阅任务 |
| 最后一个句柄 drop | 取消后台连接;需要确认清理完成时显式 await |
鉴权拒绝、服务端 `disconnect` 和手动退出不会自动重连。默认心跳间隔 25 秒,等待 pong 10 秒;连接超时 5 秒,发送总超时 15 秒,写入超时 5 秒。
默认在队列中和等待响应的 SEND 总数不超过 256,超限返回 `Backpressure`;事件保留 256 条,慢监听器收到 `RecvError::Lagged`。单个完整 JSON-RPC 报文默认限制 1 MiB,包含 Base64 膨胀。通过 `Options` 按实际负载调整。
自动 RECVACK 表示网络接收,不表示监听器已处理。即使没有监听器或监听器落后,也会确认接收;EasySDK 没有持久收件箱,需要可靠恢复时由业务层提供存储、去重和补偿。不要忽略 `Lagged`。
发送不会自动重放。超时、取消或断线时服务端是否接受可能未知;业务决定重试时保留 `SendOptions.client_msg_no` 并遵循服务端幂等约定。`SendOptions` 还支持 Header、Setting 和 Topic;`red_dot` 默认 true,尊重显式 false。群聊使用 `ChannelType::Group`,成员与权限由后端准备。
## 6. 常见问题 [#6-常见问题]
**发送超时或断线后能否直接重发?** 服务端可能已接受,先保留“结果未知”并按 `SendOptions.client_msg_no` 对账。
**监听器收到 `Lagged`?** 应用消费落后,事件已丢失。停止增长的处理队列并通过业务后端补偿,重连不会自动补历史。
使用私有 CA 时,将 DER 根证书字节加入 `Options.additional_root_certificates`。
最多 16 张,每张不超过 64 KiB,不接收私钥;公共 WebPKI 根仍保留,域名与有效期校验始终启用。
```rust
let options = wukong_easy_sdk::Options {
additional_root_certificates: vec![std::fs::read("company-root.der")?],
..Default::default()
};
```
## 可选:群聊 [#可选群聊]
可信后端通过 WuKongIM HTTP API 建群并管理成员。后端确认入群后,使用群 ID 和
`ChannelType::Group` 发送:
```rust
let ack = client.send(
"project-team",
wukong_easy_sdk::ChannelType::Group,
serde_json::json!({"type": 1, "content": "大家好!"}),
).await?;
```
接收方仍通过 `Event::Message` 获取消息,检查 `channel_type`、`channel_id`、
`from_uid` 和 `payload`。SENDACK 表示服务端接受消息,不表示每位成员已经处理。
禁止陌生人发送的群中,非成员或已移除成员发送会得到 `Error::Server { code: 3 }`,
被加入黑名单的成员得到原因码 `4`。应用应处理这些权限错误;成员管理凭证由可信后端保管。
## 下一步 [#下一步]
继续阅读[消息收发](/zh/guide/integration/messaging)与[上线检查](/zh/guide/integration/acceptance)。需要离线恢复、会话、未读或推送时,先查看 [SDK 选择](/zh/sdk)。[版本与验证记录](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md#rust)保留各次验证的完整环境和范围。
# Flutter 自定义消息 (/zh/sdk/flutter/advanced/custom-messages)
文本、图片等内置正文不能表达订单卡片、位置共享等业务数据时,可以继承 `WKMessageContent` 定义自己的正文。
## 1. 约定消息类型 [#1-约定消息类型]
为每种业务正文分配一个整数类型。客户端和服务端必须使用同一个编号;不要占用 SDK 内置的 `1`~`99`。
```dart
const orderMessageType = 10001;
```
## 2. 定义正文 [#2-定义正文]
```dart
class OrderMessageContent extends WKMessageContent {
String orderID;
String title;
OrderMessageContent({this.orderID = '', this.title = ''}) {
contentType = orderMessageType;
}
@override
Map encodeJson() => {
'order_id': orderID,
'title': title,
};
@override
WKMessageContent decodeJson(Map json) {
orderID = readString(json, 'order_id');
title = readString(json, 'title');
return this;
}
@override
String displayText() => '[订单] $title';
@override
String searchableWord() => title;
}
```
* `encodeJson` 决定发送到服务端的数据。
* `decodeJson` 把收到的 JSON 还原为对象。
* `displayText` 用于会话预览。
* `searchableWord` 用于本地搜索。
## 3. 注册后再读取消息 [#3-注册后再读取消息]
在 `setup` 完成后、连接和读取本地消息前注册一次:
```dart
await WKIM.shared.setup(options);
WKIM.shared.messageManager.registerMsgContent(
orderMessageType,
(data) => OrderMessageContent().decodeJson(
Map.from(data),
),
);
```
如果没有注册,SDK 无法把该类型还原成 `OrderMessageContent`。把所有注册集中在应用启动服务中,不要放在聊天页面里重复执行。
## 4. 发送和接收 [#4-发送和接收]
```dart
await WKIM.shared.messageManager.sendWithOption(
OrderMessageContent(orderID: 'A-1001', title: '待支付'),
WKChannel('bob', WKChannelType.personal),
WKSendOptions(),
);
```
接收时可以按类型渲染:
```dart
final content = message.messageContent;
if (content is OrderMessageContent) {
showOrderCard(content.orderID, content.title);
}
```
新增字段时保持向后兼容:旧客户端应能忽略新字段,新客户端也应为缺失字段提供默认值。
# Flutter 高级功能 (/zh/sdk/flutter/advanced)
先完成[快速开始](/zh/sdk/flutter/quickstart),再按产品需要选择高级能力。没有用到的 Provider 不需要配置。
## 上线前注意 [#上线前注意]
* `1.7.9` 使用原始 TCP,公开 API 没有认证 TLS 开关。请使用受控网络入口,或采用经过安全评审的传输方案。
* `Options.debug` 默认是 `true`。显式设为 `false`,并检查 Release 日志不会输出 Token 或消息正文。
* 本地 sqflite 数据没有 SDK 层静态加密。结合平台文件保护、备份和退出账号清理设计敏感数据保护。
* Provider 中有单一全局槽位。集中在应用级服务中安装,避免页面之间互相覆盖。
* 在真机覆盖弱网、前后台恢复、Token 失效、被踢、离线消息、账号切换和版本回滚。
# Flutter 媒体与历史消息 (/zh/sdk/flutter/advanced/media-and-history)
WuKongIMSDK 负责媒体消息的状态和传输流程,但不会替你的应用选择对象存储或历史消息 HTTP API。这两项能力都通过应用级 Provider 接入。
## 媒体上传 [#媒体上传]
先在应用启动时安装一次上传 Provider:
```dart
WKIM.shared.messageManager.addOnUploadAttachmentListener(
(message, complete) async {
final content = message.messageContent;
if (content is! WKMediaMessageContent) {
complete(false, message);
return;
}
try {
content.url = await mediaUploader.upload(content.localPath);
complete(true, message);
} catch (_) {
complete(false, message);
}
},
);
```
然后用本地文件创建并发送图片正文:
```dart
final image = WKImageContent(1080, 1920)
..localPath = '/path/to/photo.jpg';
await WKIM.shared.messageManager.sendWithOption(
image,
WKChannel('team-1', WKChannelType.group),
WKSendOptions(),
);
```
上传成功前必须把可下载地址写入 `content.url`,再调用 `complete(true, message)`。失败时调用 `complete(false, message)`,让消息进入失败状态并由 UI 提供重试入口。
## 远端历史消息 [#远端历史消息]
聊天页调用 `getOrSyncHistoryMessages` 时,如果本地数据库没有足够数据,SDK 会调用历史 Provider:
```dart
WKIM.shared.messageManager.addOnSyncChannelMsgListener(
(
channelID,
channelType,
startMessageSeq,
endMessageSeq,
limit,
pullMode,
complete,
) async {
try {
final response = await messageApi.loadHistory(
channelID: channelID,
channelType: channelType,
startMessageSeq: startMessageSeq,
endMessageSeq: endMessageSeq,
limit: limit,
pullMode: pullMode,
);
complete(toWKSyncChannelMsg(response));
} catch (_) {
complete(null);
}
},
);
```
`toWKSyncChannelMsg` 需要把业务 API 返回值转换为 `WKSyncChannelMsg`,包括 `startMessageSeq`、`endMessageSeq`、`more` 和 `messages`。每条消息转换为 `WKSyncMsg` 时,保留服务端消息 ID、序号、发送者、频道、时间和 payload。
再次调用会覆盖上一次设置。请在应用级 IM 服务中集中安装,并保证每条代码路径都会调用 `complete`。
# HarmonyOS 自定义消息 (/zh/sdk/harmonyos/advanced/custom-messages)
文本、图片等内置正文不能表达订单卡片、位置共享等业务数据时,可以继承 `WKMessageContent` 定义自己的正文。
## 1. 约定消息类型 [#1-约定消息类型]
客户端和服务端为每种业务正文使用同一个整数编号。不要占用 SDK 内置的 `1`~`99`。
```typescript
const ORDER_MESSAGE_TYPE: number = 10001
```
## 2. 定义正文 [#2-定义正文]
```typescript
import { CommonUtil } from '@wukong/wkim/src/main/ets/common/CommonUtil'
import { WKMessageContent } from '@wukong/wkim/src/main/ets/model/WKMessageContent'
export class OrderMessageContent extends WKMessageContent {
orderId: string = ''
title: string = ''
constructor(orderId: string = '', title: string = '') {
super()
this.orderId = orderId
this.title = title
this.contentType = ORDER_MESSAGE_TYPE
}
encodeJson(): Record {
return {
'order_id': this.orderId,
'title': this.title
}
}
decodeJson(jsonStr: string): WKMessageContent {
const json = CommonUtil.jsonToRecord(jsonStr)
if (json !== undefined) {
this.orderId = CommonUtil.readString(json, 'order_id') ?? ''
this.title = CommonUtil.readString(json, 'title') ?? ''
}
return this
}
displayText(): string {
return `[订单] ${this.title}`
}
searchableWord(): string {
return this.title
}
}
```
* `encodeJson` 决定发送到服务端的数据;
* `decodeJson` 把收到的 JSON 字符串还原为对象;
* `displayText` 用于会话预览;
* `searchableWord` 用于本地搜索。
## 3. 注册后再读取消息 [#3-注册后再读取消息]
在 `init` 完成后、连接和读取本地消息前注册一次:
```typescript
WKIM.shared.messageManager().registerMsgContent(
ORDER_MESSAGE_TYPE,
(jsonStr: string): WKMessageContent => {
return new OrderMessageContent().decodeJson(jsonStr)
}
)
```
如果没有注册,SDK 会把正文当成未知类型。把所有注册集中在应用启动服务中,不要在聊天页面里重复执行。
## 4. 发送和接收 [#4-发送和接收]
```typescript
WKIM.shared.messageManager().send(
new OrderMessageContent('A-1001', '待支付'),
new WKChannel('bob', WKChannelType.personal)
)
```
接收时按 ArkTS 类型渲染:
```typescript
const content = message.messageContent
if (content instanceof OrderMessageContent) {
showOrderCard(content.orderId, content.title)
}
```
新增字段时保持向后兼容:旧客户端应能忽略新字段,新客户端也应为缺失字段提供默认值。
# HarmonyOS 高级功能 (/zh/sdk/harmonyos/advanced)
先完成[快速开始](/zh/sdk/harmonyos/quickstart),再按产品需要选择高级能力。没有使用的 Provider 不需要配置。
## 上线前注意 [#上线前注意]
* 当前长连接使用原始 TCP,公开配置没有认证 TLS 开关。请使用受控网络入口,或采用经过安全评审的传输方案。
* `WKConfig.debug` 不能关闭当前 SDK 日志。检查 Release 日志,避免输出 Token、消息正文和用户标识。
* 本地 `RelationalStore` 没有启用 SDK 层静态加密。结合平台文件保护、备份策略和退出账号清理保护敏感数据。
* `WKIM.shared`、Manager 和 Provider 都是进程全局状态。集中在一个应用级服务中安装,不要由页面相互覆盖。
* 当前版本没有统一重置所有 Provider 和回调。切换 UID 前调用 `disConnection(true)` 并验证状态隔离;无法确认时,重启进程后再初始化新账号。
* 在真机覆盖弱网、前后台恢复、Token 失效、被踢、离线消息、账号切换和版本回滚。
# HarmonyOS 媒体与历史消息 (/zh/sdk/harmonyos/advanced/media-and-history)
SDK 管理媒体消息的状态和传输流程,但不会替你的应用选择对象存储或历史消息 HTTP API。这两项能力都通过 `WKIM.shared.config.provider` 接入。
## 媒体上传 [#媒体上传]
在应用启动时安装一次上传 Provider:
```typescript
WKIM.shared.config.provider.uploadAttachmentCallback = async (
message: WKMsg
): Promise<[boolean, WKMsg]> => {
const content = message.messageContent
if (!(content instanceof WKImageContent)) {
return [false, message]
}
try {
content.url = await mediaUploader.upload(content.localPath)
message.messageContent = content
return [true, message]
} catch (_) {
return [false, message]
}
}
```
然后用本地文件创建图片正文:
```typescript
const image = new WKImageContent('/data/storage/el2/base/files/photo.jpg')
image.width = 1080
image.height = 1920
WKIM.shared.messageManager().send(
image,
new WKChannel('team-1', WKChannelType.group)
)
```
Provider 成功前必须把可下载地址写入 `content.url`。返回 `[false, message]` 时,消息会进入失败状态,UI 应提供重试入口。
图片正文已由 SDK 注册。语音和视频模型需要先用 `registerMsgContent` 注册,再在同一个上传 Provider 中按正文类型处理。
## 远端历史消息 [#远端历史消息]
设置历史 Provider,把业务 API 返回值转换为 `WKSyncChannelMsg`:
```typescript
WKIM.shared.config.provider.syncMessageCallback = async (
channel: WKChannel,
options: SyncOptions
): Promise => {
const response = await messageApi.sync({
channelId: channel.channelId,
channelType: channel.channelType,
startMessageSeq: options.startMessageSeq,
endMessageSeq: options.endMessageSeq,
limit: options.limit,
pullMode: options.pullMode
})
return toWKSyncChannelMsg(response)
}
```
转换结果包括 `startMessageSeq`、`endMessageSeq`、`more` 和 `msgs`。每条 `WKMsg` 应保留服务端消息 ID、序号、发送者、频道、时间和正文 JSON。
聊天页继续调用 `getOrSyncHistoryMessages`。SDK 会先读取本地数据,需要时再调用这个 Provider,并把返回消息保存到本地。
再次赋值会替换上一次实现。请在应用级 IM 服务中集中设置,并让 Promise 在成功和失败时都能结束。
# iOS 自定义消息 (/zh/sdk/ios/advanced/custom-messages)
文本和图片之外的内容,例如订单卡片或位置分享,可以实现为 `WKMessageContent` 子类。所有客户端必须为同一种正文使用相同的类型编号和 JSON 字段。
## 1. 定义正文 [#1-定义正文]
下面用订单卡片演示。`10001` 只是示例;请在团队内建立类型编号表,避免与其他消息重复。
```objc
@interface OrderMessageContent : WKMessageContent
@property (nonatomic, copy) NSString *orderID;
@property (nonatomic, copy) NSString *title;
@end
@implementation OrderMessageContent
+ (NSNumber *)contentType {
return @(10001);
}
- (NSDictionary *)encodeWithJSON {
return @{
@"order_id": self.orderID ?: @"",
@"title": self.title ?: @""
};
}
- (void)decodeWithJSON:(NSDictionary *)json {
self.orderID = [json[@"order_id"] isKindOfClass:[NSString class]]
? json[@"order_id"] : @"";
self.title = [json[@"title"] isKindOfClass:[NSString class]]
? json[@"title"] : @"";
}
- (NSString *)conversationDigest {
return [NSString stringWithFormat:@"[订单] %@", self.title];
}
- (NSString *)searchableWord {
return self.title;
}
@end
```
解码来自网络的数据时要检查类型和缺失字段,不能假设每个客户端都已经升级到同一版本。
## 2. 在读取消息前注册 [#2-在读取消息前注册]
```objc
[[WKSDK shared] registerMessageContent:[OrderMessageContent class]];
```
在 App 启动并配置 SDK 时注册一次,且要早于连接和读取本地消息。未注册的正文无法还原成你的业务类型。
## 3. 发送和展示 [#3-发送和展示]
```objc
OrderMessageContent *content = [OrderMessageContent new];
content.orderID = @"order-2026-001";
content.title = @"咖啡豆";
WKChannel *channel = [WKChannel personWithChannelID:@"bob"];
[[WKSDK shared].chatManager sendMessage:content channel:channel];
```
接收时按类型渲染;未知类型要显示通用占位,而不是崩溃:
```objc
if ([message.content isKindOfClass:[OrderMessageContent class]]) {
OrderMessageContent *order = (OrderMessageContent *)message.content;
[self showOrderWithID:order.orderID title:order.title];
} else {
[self showUnsupportedMessage];
}
```
修改字段时保持向后兼容:新增字段给默认值,不复用已有字段表达新的含义,也不要更改已经发布的类型编号。
# iOS 高级功能 (/zh/sdk/ios/advanced)
先完成[快速开始](/zh/sdk/ios/quickstart),再按产品需要选择高级能力。没有用到的 Provider 不需要配置。
## 上线前注意 [#上线前注意]
* `1.1.1` 的默认连接是原始 TCP,公开配置中没有认证 TLS 开关。请在受控网络入口使用,或升级到经过你们安全评审的传输方案。
* 设置 `options.isDebug = NO`,并检查 Release 日志不会输出 Token 或消息正文。
* 本地消息库按 UID 隔离,但 UID 不是加密密钥。结合 iOS 文件保护、备份策略和退出账号后的数据清理一起设计。
* 在真机上覆盖弱网、后台恢复、Token 失效、被踢、离线消息和版本回滚。
# iOS 媒体与历史消息 (/zh/sdk/ios/advanced/media-and-history)
图片消息包含本地文件和可跨设备访问的远端地址。SDK 管理消息状态和任务调度,你的应用负责真正的对象存储上传、下载和鉴权。
## 发送图片 [#发送图片]
先创建图片正文,再像文本一样发送:
```objc
WKImageContent *content = [WKImageContent initWithImage:image];
WKChannel *channel = [WKChannel groupWithChannelID:@"team-1"];
WKMessage *message = [[WKSDK shared].chatManager sendMessage:content
channel:channel];
```
在发送图片前配置上传任务工厂:
```objc
[WKSDK shared].mediaManager.uploadTaskProvider = ^id(WKMessage *message) {
return [[AppMediaUploadTask alloc] initWithMessage:message
storage:objectStorage];
};
```
`AppMediaUploadTask` 是应用实现的任务,不是 SDK 类型。它需要遵守 `WKTaskProto`,把文件上传到你的存储服务,更新媒体正文中的远端地址,并向 SDK 报告成功或失败。上传凭证应短期有效,并限制文件路径、大小和类型。
## 下载媒体 [#下载媒体]
同样提供下载任务,然后让管理器执行:
```objc
[WKSDK shared].mediaManager.downloadTaskProvider = ^id(WKMessage *message) {
return [[AppMediaDownloadTask alloc] initWithMessage:message
storage:objectStorage];
};
[[WKSDK shared].mediaManager download:message
callback:^(WKMediaDownloadState state,
CGFloat progress,
NSError *error) {
[self updateDownloadProgress:progress state:state];
}];
```
缓存文件前校验大小和类型;退出账号时按产品策略清理私有媒体缓存。
## 补齐远端历史消息 [#补齐远端历史消息]
`pullLastMessages:` 会先读本地库。当本地范围不足时,可通过 `syncChannelMessageProvider` 接入你的历史消息 API:
```objc
[WKSDK shared].chatManager.syncChannelMessageProvider = ^(
WKChannel *channel,
uint32_t startMessageSeq,
uint32_t endMessageSeq,
NSInteger limit,
WKPullMode pullMode,
WKSyncChannelMessageCallback callback
) {
[historyAPI fetchChannel:channel
start:startMessageSeq
end:endMessageSeq
limit:limit
mode:pullMode
completion:callback];
};
```
业务 API 需要返回 `WKSyncChannelMessageModel`。始终调用 `callback`;失败时传入错误,让调用方结束加载状态并允许重试。
## 常见问题 [#常见问题]
* 图片一直是上传中:确认任务最终报告成功或失败,并且上传后写回远端地址。
* 其他设备看不到图片:不要把沙盒本地路径当成远端 URL。
* 历史列表重复:以频道和 `messageSeq` 去重,不要只按数组位置合并。
* 翻页结束不了:服务端必须正确表达没有更多数据,Provider 也必须结束回调。
# JavaScript / Web 自定义消息 (/zh/sdk/javascript/advanced/custom-messages)
文本和图片之外的内容,例如订单卡片或位置分享,可以继承 `MessageContent`。所有客户端必须为同一种正文使用相同的类型编号和 JSON 字段。
## 1. 定义正文 [#1-定义正文]
`10001` 只是示例;请在团队内建立类型编号表,避免与其他消息重复。
```typescript
class OrderMessageContent extends MessageContent {
orderID = ''
title = ''
constructor(orderID = '', title = '') {
super()
this.orderID = orderID
this.title = title
}
get contentType(): number {
return 10001
}
get conversationDigest(): string {
return `[订单] ${this.title}`
}
encodeJSON() {
return {
order_id: this.orderID,
title: this.title,
}
}
decodeJSON(value: Record) {
this.orderID = typeof value.order_id === 'string' ? value.order_id : ''
this.title = typeof value.title === 'string' ? value.title : ''
}
}
```
解码时检查类型和缺失字段,因为不同用户可能运行不同应用版本。
## 2. 在连接前注册 [#2-在连接前注册]
```typescript
const ORDER_CONTENT_TYPE = 10001
sdk.register(ORDER_CONTENT_TYPE, () => new OrderMessageContent())
```
每个页面上下文注册一次。未注册的正文会被解码成未知类型。
## 3. 发送和展示 [#3-发送和展示]
```typescript
await sdk.chatManager.send(
new OrderMessageContent('order-2026-001', '咖啡豆'),
new Channel('bob', ChannelTypePerson),
)
```
接收时判断实际类型:
```typescript
if (message.content instanceof OrderMessageContent) {
showOrder(message.content.orderID, message.content.title)
} else {
showUnsupportedMessage()
}
```
新增字段时给默认值,不复用旧字段表达新含义,也不要修改已经发布的类型编号。跨 iOS、Android、Flutter 或 HarmonyOS 时,字段名和编号必须完全一致。
# JavaScript / Web 高级功能 (/zh/sdk/javascript/advanced)
先完成[快速开始](/zh/sdk/javascript/quickstart),再按产品需要选择高级能力。
## 上线前注意 [#上线前注意]
* 生产网页使用 `wss://`,验证证书、反向代理 WebSocket Upgrade 和 CSP。
* `1.3.5` 的部分控制台输出不受 `config.debug` 控制,可能包含解码后的消息数据。发布前应采用经过评审的删除或脱敏方案,并检查最终 bundle。
* Token 只放内存或受控会话存储,避免 URL、分析事件、错误上报和浏览器日志。
* 覆盖断网重连、标签页休眠、Token 失效、被踢、离线去重、账号切换和版本回滚。
# JavaScript 离线恢复与 UniApp 迁移 (/zh/sdk/javascript/advanced/offline-and-uniapp)
JavaScript SDK 不自带持久化消息数据库。页面重载、断线期间的消息和历史翻页,都需要你的业务服务端返回数据。
## 离线恢复 [#离线恢复]
先实现消息同步 Provider:
```typescript
sdk.config.provider.syncMessagesCallback = async (channel, options) => {
const rows = await messageApi.sync({
channelID: channel.channelID,
channelType: channel.channelType,
startMessageSeq: options.startMessageSeq,
endMessageSeq: options.endMessageSeq,
limit: Math.min(options.limit, 100),
pullMode: options.pullMode,
})
return rows.map(toSDKMessage)
}
```
重连成功后,从上次保存的消息序号继续同步,并按 `messageID` 或频道内 `messageSeq` 去重:
```typescript
const options = new SyncOptions()
options.startMessageSeq = lastMessageSeq
options.limit = 50
options.pullMode = PullMode.Up
const recovered = await sdk.chatManager.syncMessages(channel, options)
mergeMessages(recovered)
```
同步失败要保留游标并允许重试,不能先推进游标再丢弃未处理消息。
## 从旧 UniApp SDK 迁移 [#从旧-uniapp-sdk-迁移]
官方 `wukongimuniappsdk` 已停止维护。新项目不要安装它;现有项目按下面顺序迁移:
```bash
npm uninstall wukongimuniappsdk
npm install --save-exact wukongimjssdk@1.3.5
```
统一从包根导入,不使用 `wukongimjssdk/lib/*`:
```typescript
import WKSDK, {
Channel,
ConnectStatus,
MessageText,
} from 'wukongimjssdk'
```
然后逐项迁移:
1. 设置 `uid`、`token`、WebSocket 地址和设备类型;
2. 重新配置消息、会话和频道 Provider;
3. 在连接前注册自定义正文;
4. 改用 `addConnectStatusListener`、`addMessageListener` 和 `addMessageStatusListener`;
5. 改用 `chatManager.send(...)`;
6. 组件卸载时移除相同函数引用。
### UniApp 运行时注意 [#uniapp-运行时注意]
`1.3.5` 会按全局对象自动选择 `uni.connectSocket`、`wx.connectSocket` 或原生 `WebSocket`。不要手动设置 `config.platform` 来强制选择;当前实现中该值不会同步初始化 socket 工厂,可能造成创建方式和事件 API 不一致。
按目标设置设备类型:原生 App 容器通常是 `0`,H5/Web 是 `1`,桌面端是 `2`。小程序类型应由你的产品协议决定,不要仅凭“它使用 JavaScript”猜测。
App、H5 和每一种小程序都是不同运行时。分别验证 WSS 域名白名单、证书、前后台、网络切换、在线消息、断线恢复和去重;一个目标通过不代表其他目标也通过。
# Authentication & Security (/en/api/authentication)
WuKongIM HTTP API is for trusted backends. Browsers connect to Gateway through the SDK or request server capabilities through a same-origin BFF.
## Trust boundaries [#trust-boundaries]
The browser should receive only the UID, temporary token, and WebSocket address needed for its current connection. It must not receive the WuKongIM HTTP API address or a server credential.
`gateway.token_auth_on` defaults to `true`. After `POST /user/token` stores a device token, a later CONNECT for the same UID and `device_flag` must carry the exact value. This built-in check has no expiry timestamp or replay policy, so production must still protect token-management routes and own issuance, rotation, and revocation.
## Production requirements [#production-requirements]
| Capability | Owner |
| ------------------------------------------------------------------ | --------------------------------------------- |
| Account, tenant, UID, and device policy | Product identity service and backend |
| Token generation, expiry, revocation, and secure storage | Product backend |
| CONNECT token validation | Gateway |
| WuKongIM HTTP API authentication, authorization, limits, and audit | API Gateway, service mesh, or product backend |
| TLS, key rotation, and alerts | Production platform |
WuKongIM HTTP API accepts any Origin for compatibility, but the routes have no business authentication. Do not treat CORS response headers as a security boundary or let browsers call these routes directly. The reverse proxy must restrict network sources and bound request-body size, request/response time, rate, concurrency, and audit volume—especially for unbounded UID arrays and payloads.
The durable system-UID set and effective permission caches are also different observations. `systemuids_add/remove` synchronously updates only the handling process cache; refresh other nodes individually or roll them. `systemuids_add_to_cache/remove_from_cache` affects only the current process and requires a node-directed, protected operator entry.
Protocol encryption does not replace TLS, account authentication, or HTTP service authorization. Tokens must not appear in URLs, logs, or screenshots.
Bind the sample BFF to loopback and validate Host/Origin. Do not deploy it to the public internet, shared test environments, or real-user traffic.
Related references: [`/user/token`](/en/api/product-http/users), [Error Responses](/en/api/product-http/errors), and [Integration Architecture](/en/guide/integration/architecture).
# API Versions & Compatibility (/en/api/compatibility)
API pages describe contracts for the current build. An unchanged URL does not make different server, SDK, or contract versions automatically compatible.
## Current snapshot [#current-snapshot]
The snapshot's verification status covers only this basic integration flow:
| Operation | Verified behavior |
| --------------------------- | ------------------------------------------ |
| `POST /user/token` | Create a development identity |
| `GET /route` | Discover WebSocket ingress |
| `POST /channel/messagesync` | Recover committed messages after reconnect |
The complete [WuKongIM HTTP API OpenAPI](/contracts/product-http.openapi.json) covers all 44 current registrations; Operations and Webhooks have separate contracts. Three narrow profiles remain available for controlled adoption, but the end-to-end status above covers only the three basic-integration operations. Source alignment, Schema checks, and protocol pages do not expand that runtime evidence.
## Version rules [#version-rules]
* Revalidate the application flow after changing the server, SDK, or contract.
* Treat an unverified combination as unknown, not compatible by default.
* Use the complete contract for the current source surface and a narrow profile for a specific adoption scope.
Compatibility does not make an API safe for public exposure. Keep WuKongIM HTTP API behind a trusted network or authenticated proxy; see the [Interface Inventory](/en/api/interface-inventory) for private and experimental boundaries.
## Compatibility snapshot (shared contract)
- Documentation channel: `v3-beta-snapshot`
- Server source revision: `b469c6e5e2b4ff65611a8fe3389fb5034de3f152`
- Verification status: unverified (no verification receipt supplied)
- Receipt schema: `wukongim.docs.golden-path-verification/v1`
- Topology: single-node cluster
- Physical hash slots: 256
- JavaScript SDK: `wukongimjssdk@1.3.5`
- Golden scenario: `javascript-web-quickstart/alice-bob-reconnect-sync/v1`
- Sample Node.js requirement: `>=20.11`
- Sample lockfile SHA-256: `1aabcbf3a92b5658f7f7cad0abe21a8dbcbfc7ecaab8387665bf3e2ccb0af3f7`
- Test runtime target: Node.js 22.12.0, npm
- Chromium test target: chromium 151.0.7922.34 (revision 1234) via `@playwright/test@1.62.1`
- Other browsers: unverified
- [compatibility.json](/compatibility.json)
- [OpenAPI subset](/contracts/javascript-web-quickstart.openapi.json)
# Conventions (/en/api/conventions)
The following rules apply to the currently published WuKongIM HTTP API operations.
## Addressing and format [#addressing-and-format]
| Convention | Behavior |
| ---------------- | ----------------------------------------------------------------------------------------------------- |
| Base URL | Local example: `http://127.0.0.1:5001`; all paths start at `/` |
| Request | POST uses UTF-8 JSON; `/route`, `/channel/whitelist`, and `/user/systemuids` use GET query parameters |
| Response | JSON; endpoints do not share a universal envelope |
| UID / Channel ID | Strings owned by the product |
| Channel | Identified by `channel_id` plus `channel_type` |
| Message sequence | Ordered only within one Channel |
Check HTTP status first, then parse the endpoint-specific schema. Do not force responses into a universal `{data,error}` type.
## Identifiers and cursors [#identifiers-and-cursors]
* `uid` is a product identity, not a nickname, connection ID, or device ID.
* The client view of a person Channel uses the peer UID.
* `message_seq` is a per-Channel `uint64` cursor. JavaScript must preserve it with a lossless JSON parser, decimal string, or `BigInt`. `message_idstr` mirrors only `message_id`, cannot replace `message_seq`, and is absent from `/message/send` responses.
* `next_cursor` is opaque. Echo it unchanged until `done=true`.
For legacy-client compatibility, request objects ignore unknown JSON fields. Callers should still send only declared fields: a typo will not necessarily fail, and extra fields are not an extension mechanism.
## State and execution scope [#state-and-execution-scope]
| Scope | Meaning |
| --------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Cluster-durable state | Other nodes can read it after success, while follow-up derived work may still be running |
| Current-process cache | Affects only the process handling the request; do not assume it survives load-balancer node changes |
| Owner-local action | The request node routes or delays a Session action; the HTTP response does not mean that action has finished |
| Staged write | Removal, chunked insertion, and derived-flag refresh are not one transaction; verify desired state after failure |
The “Operation boundary” callout on each generated operation page identifies the applicable scope and any success check beyond HTTP 200.
## Retries [#retries]
| Operation | Rule |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `/route` | Back off and retry network failures or temporary `5xx` |
| `/user/token` | Retry the same identity intent; do not mint tokens in a loop |
| `/channel/messagesync` | Retry with the same cursor and tolerate overlap with realtime delivery |
| Channel mutation | Replay the desired state only while the product intent remains valid; `reset`, set, and remove-all may partially complete |
| `/conversation/list` | Echo `next_cursor` unchanged and stop only at `done=true` |
| `/conversation/list` | Any channel read failure fails the whole page; retry the original request and cursor |
These operations define no universal `Idempotency-Key`. See [Authentication & Security](/en/api/authentication) for security requirements and [Error Responses](/en/api/product-http/errors) for failure classification.
## JavaScript Web golden-path contract
This is a non-exhaustive v3 Beta subset. These three calls belong only in the trusted localhost BFF; the browser must not call the WuKongIM HTTP API directly.
- `POST /user/token`
- `GET /route`
- `POST /channel/messagesync`
[OpenAPI 3.1 subset](/contracts/javascript-web-quickstart.openapi.json)
# API & Protocols (/en/api)
This section classifies every current source interface by audience and protocol. Public contracts are downloadable; private, conditional, and experimental entries are documented as boundaries, not compatibility promises.
## Quick links [#quick-links]
## Usage boundaries [#usage-boundaries]
* WuKongIM HTTP API is for trusted backends only; do not expose it directly to browsers or mobile clients.
* Operations HTTP and Manager belong on an isolated operator network; they are separate contracts.
* JSON-RPC supports CONNECT, ping, online SEND/SENDACK, RECV/RECVACK, and server-to-client public stream.\* EVENT for EasySDK; batches, subscriptions, offline sync, and general-purpose RPC are outside that contract.
* HTTP status, endpoint responses, and CONNACK/SENDACK Reason Codes belong to separate layers and must be handled separately.
* See [API Compatibility](/en/api/compatibility) for version coverage and [Error Responses](/en/api/product-http/errors) for failure handling.
# Interface Inventory & Trust Boundaries (/en/api/interface-inventory)
This page answers “what other entry surfaces exist in source?” It inventories boundaries; it does not promote internal interfaces into public compatibility promises.
## Manager: 110 private routes [#manager-110-private-routes]
The authoritative registrations are `internal/access/manager/server.go`, `backups.go`, and `restore.go`.
| Group | Count | Permission with `auth_on=true` | With `auth_on=false` |
| ------------------------ | -----: | ------------------------------------------------- | ----------------------------------- |
| Operations MCP endpoint | 1 | Dedicated MCP Bearer | Dedicated MCP Bearer |
| Login info | 1 | Public configured guest credentials | Not registered |
| Login | 1 | Fixed-user login | Not registered |
| Permissions | 1 | `cluster.permission:r` | Unguarded |
| MCP read / write | 2 / 5 | `cluster.mcp:r` / `cluster.mcp:w` | Fail closed |
| Node read / write | 9 / 12 | `cluster.node:r` / `cluster.node:w` | Unguarded |
| Slot read / write | 3 / 3 | `cluster.slot:r` / `cluster.slot:w` | Unguarded |
| Controller read / write | 6 / 3 | `cluster.controller:r` / `cluster.controller:w` | Unguarded |
| Diagnostics read / write | 4 / 2 | `cluster.diagnostics:r` / `cluster.diagnostics:w` | Unguarded |
| Application log read | 3 | `cluster.log:r` | Unguarded |
| Database inspect | 3 | `cluster.db:r` | Unguarded |
| Channel read / write | 8 / 12 | `cluster.channel:r` / `cluster.channel:w` | Unguarded |
| Migration read | 2 | `cluster.channel:r` | Unguarded |
| Connection read | 2 | `cluster.connection:r` | Unguarded |
| Webhook config read | 1 | `cluster.webhook:r` | Unguarded |
| Plugin read / write | 3 / 5 | `cluster.plugin:r` / `cluster.plugin:w` | Unguarded |
| User read / write | 3 / 4 | `cluster.user:r` / `cluster.user:w` | Unguarded |
| Backup read / write | 2 / 7 | `cluster.backup:r` / `cluster.backup:w` | Reads unguarded; writes fail closed |
| Restore write | 2 | Exact `cluster.restore:w` | Fail closed |
Total: `110`. `POST /manager/slots/leader-transfer-plan` is a planning-only route under read permission. The MCP handler behind `ANY /mcp` accepts only POST and rejects non-empty Origin; all other Manager routes use open CORS. `lib/api-surface-contracts.ts` retains the complete method/path set and a source test checks it.
`GET /manager/login` returns only the configured `guest` credentials to the login page, with `Cache-Control: no-store`; other accounts remain private.
`GET /manager/nodes/:node_id/config/toml` returns a versioned, redacted startup TOML document under the same `cluster.node:r` read permission.
## Node transport: 77 shared IDs [#node-transport-77-shared-ids]
The audience is **cluster-internal**. The shared TCP transport has no per-call Bearer or TLS identity. Only join validates the Join Token; other calls trust cluster routing and an isolated network.
| ID | Catalog names |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `1–31` | `slot_forward_propose`, `channel_pull`, `channel_ack`, `channel_pull_hint`, `channel_notify`, `control_state_sync`, `control_report_node`, `control_report_slots`, `channel_append`, `channel_append_batch`, `control_raft`, `control_task_result`, `presence_authority`, `presence_owner`, `delivery_push`, `delivery_fanout`, `channel_pull_batch`, `channel_pull_hint_batch`, `channel_last_visible`, `reserved_conversation`, `channel_authority_send`, `manager_connection`, `manager_logs`, `manager_controller_raft`, `manager_slot_raft`, `manager_channels`, `manager_db_inspect`, `manager_app_logs`, `manager_diagnostics`, `manager_plugins`, `plugin_binding_scan` |
| `32–33` | `msg_slot_raft`, `msg_slot_raft_batch` |
| `64–86` | `control_write`, `manager_message_retention`, `node_lifecycle`, `slot_status`, `manager_task_audit`, `channel_migration_meta`, `message_event_append`, `manager_node_config`, `manager_latest_messages`, `scheduled_backup_messages`, `scheduled_backup_slot`, `scheduled_backup_probe`, `scheduled_backup_restore`, `operations_mcp`, `manager_goroutines`, `slot_subscriber_metadata`, `slot_channel_metadata`, `channel_conversation_heads`, `channel_committed_reads`, `slot_user_membership`, `slot_runtime_metadata`, `slot_permission_metadata_batch`, `channel_quorum_exchange` |
| `87` | `slot_identity_metadata` |
| `88` | `manager_node_config_document` |
| `89` | `slot_message_updates` |
| `90` | `message_update_hint` |
| `91` | `node_send_permissions` |
| `92` | `message_event_delivery` |
| `93` | `mqtt_source` |
| `94` | `channel_mqtt_replay` |
| `95` | `channel_mqtt_copy` |
| `96` | `channel_mqtt_anchor` |
| `97` | `channel_mqtt_plan` |
| `98` | `channel_mqtt_repair` |
| `99` | `channel_mqtt_recovery` |
| `100` | `channel_mqtt_retirement` |
| `101` | `channel_mqtt_retirement_selection` |
| `102` | `channel_mqtt_consumer_read` |
| `103` | `channel_will_receipt` |
| `104` | `channel_mqtt_originals` |
| `105` | `mqtt_will_dispatch` |
| `106` | `slot_mqtt_metadata` |
| `107` | `mqtt_owner` |
ID `16` is retired-reserved and ID `20` reserves a removed feature; neither may be reused. The default Slot proxy uses promoted IDs `79/80/83/84/85/87/89`. The default identity handler accepts only `get_device`; user reads and scans remain outside this runtime surface. Identity metadata uses ID `87`, avoiding the former collision with `channel_pull_hint` at `4`. The generic, non-default Slot Store still declares private IDs `47/53`; these remain catalog debt. Keep these protocols in an unstable internal inventory and never generate fake OpenAPI paths for them.
## MCP [#mcp]
| Surface | Exact tools | Boundary |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Operations MCP `POST /mcp` | `cluster_health`, `node_inspect`, `slot_inspect`, `channel_runtime_inspect`, `controller_tasks_query`, `metrics_query_range`, `logs_search`, `logs_context`, `diagnostics_query`, `config_read_redacted`, `backup_inspect`, `pprof_analyze` | Dedicated `wko_*` Bearer; Manager JWT rejected; non-empty Origin rejected; read-only except bounded `pprof_analyze` |
| Cloud Analysis MCP `POST /mcp` | `run_inspect`, `cluster_snapshot`, `workload_inspect`, `metrics_query_range`, `logs_search`, `logs_context`, `diagnostics_query`, `task_audits_query`, `trace_start`, `trace_query`, `profile_capture`, `profile_top`, `profile_list`, `config_read_redacted` | Non-renewable Analysis Bearer scoped to one live Run; `trace_start` and `profile_capture` are bounded active diagnostics |
| Review Check stdio MCP | `check_list`, `check_result`, `check_run` | Local credential-free stdio; named checks only, never arbitrary commands, arguments, or paths |
The Cloud Analysis Gateway also exposes unauthenticated `GET /healthz`, unauthenticated `GET /self-check`, and optional GitHub-OIDC `POST /analysis/token`. All are **agent-internal**, not public integration APIs.
## Cloud View [#cloud-view]
Cloud View is an unauthenticated Simulation Viewer reverse proxy. `ANY /cloud-view/status` returns no-store state, `/prometheus[/...]` proxies Prometheus, and any WebSocket Upgrade proxies Gateway. Known Product prefixes proxy API; every other path defaults to Manager. It adds per-IP/global limits and WS concurrency bounds, but no identity. `X-WuKongIM-Cloud-View-Gate` affects benchmark-purity marking only; it does not authorize a request.
## Agent and benchmark controls [#agent-and-benchmark-controls]
* `wkissueagent`: `reconcile-github`, `recover-task`, `build-context`, `capture-candidate`, `verify-candidate`, `mint-app-token`, `publish-candidate`.
* `wkreviewagent`: `normalize-review-result`, `reconcile-github`, `recover-review`, `build-context`, `verify-baseline`, `validate-review-result`, `validate-explanation`, `append-state`, `publish-review`.
* `wkreviewcheck`: `go-format`, `go-mod-tidy`, `web`, `demo`, `docs`, `docs-integration`, `three-node`.
* Generic benchmark worker: 14 `/healthz` and `/v1/*` control routes; all except Health require a Control Token or explicitly enabled insecure control.
* Chat-lifecycle worker: 12 `/healthz`, `/v1/info`, and `/v1/chat-lifecycle/*` routes; every endpoint, including Health, requires Bearer authentication.
These are strict JSON/stdin or private HTTP process contracts. Maintain them only in agent and benchmark inventories.
## Public integration boundary [#public-integration-boundary]
| Surface | Audience | Publication decision |
| --------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------- |
| Plugin host RPC: 8 Unix-socket routes | Plugin author | Separate Beta Plugin Protocol; protobuf/wkrpc, not OpenAPI |
| Webhook: `msg.notify`, `msg.offline`, `user.onlinestatus` | Public integration | Publish a callback contract; currently POST JSON, HTTP 200-only success, no signature |
| Operations HTTP | Operator | Separate operations documentation; never mix with WuKongIM HTTP API |
| Manager, Node, Cloud View, Cloud Analysis, Agent, Bench | Private | Document boundary, stability, and risk only; no public compatibility promise |
The exact local plugin paths are `/plugin/start`, `/close`, `/message/send`, `/channel/messages`, `/cluster/config`, `/cluster/channels/belongNode`, `/conversation/channels`, and `/plugin/httpForward`. Caller identity comes from the Unix-socket wkrpc UID. Webhook delivery uses best-effort in-memory retry with no crash replay; receivers must add source authentication and idempotency.
# Guides (/en/guide)
WuKongIM is open-source, self-hosted real-time messaging infrastructure. These guides take you from product evaluation through your first run, the core concepts, and integration with an existing application.
New to WuKongIM? Read the product overview first. Ready to try it? Go directly to the quick start.
## Recommended paths [#recommended-paths]
Still deciding whether it fits? Read the [product overview](/en/guide/product-overview). Ready to validate it? Use the [quick start](/en/guide/quick-start) to launch a cluster, check readiness, and exchange the first two-way message.
# Choose an SDK (/en/sdk)
WuKongIM provides two client SDK families. Choose by how much client-side state the SDK should manage, then open the matching guide.
## Which one should I use? [#which-one-should-i-use]
| Your requirement | Recommended |
| -------------------------------------------------------- | ------------- |
| The SDK should maintain local messages and the chat list | WuKongIMSDK |
| You need unread counts, history, and offline recovery | WuKongIMSDK |
| You only need an online connection plus send and receive | WuKongEasySDK |
| You want the smallest possible integration surface | WuKongEasySDK |
For a complete chat product, start with [WuKongIMSDK](/en/sdk/wukongim). To add lightweight realtime communication to an existing product, start with [WuKongEasySDK](/en/sdk/easy).
# Server (/en/server)
Server documentation is for deployers, operators, and engineers who need to understand the system internals.
Every deployment is a cluster. A deployment with one node is a single-node cluster; there is no standalone path that bypasses cluster behavior.
Start a new environment from Deployment; use Configuration, Operations, and Architecture as server references when needed. Publication does not replace deployment, recovery, or upgrade execution evidence for the target environment.
## Documentation areas [#documentation-areas]
# Connection Lifecycle (/en/api/client-protocols/connection-lifecycle)
## Establish a connection [#establish-a-connection]
* `CONNECT` must be the sole first packet. A batch tail or another packet while authentication is pending closes the connection.
* The server negotiates the protocol version (currently v6; client values `0` or above v6 select v6), stores UID and device state plus encryption Session state when enabled, then activates online Presence.
* A non-success `CONNACK` is written before close. If a successful `CONNACK` cannot be written, completed activation is rolled back.
Success proves only that the protocol Session exists. Recover durable messages and merge local state before ordinary product sends are enabled.
## Active session [#active-session]
| Direction | Exchange | Completion boundary |
| --------------- | ------------------ | ----------------------------------------------------------- |
| Client → Server | `PING` → `PONG` | Heartbeat response only |
| Client → Server | `SEND` → `SENDACK` | Protocol send result; not peer receipt or product execution |
| Server → Client | `RECV` → `RECVACK` | Session receive feedback; not end-user read state |
The default read-idle timeout is three minutes. Only inbound activity refreshes it; server outbound traffic does not. Proxy and load-balancer idle policies must accommodate the effective heartbeat.
## Close and recover [#close-and-recover]
* `ReasonAuthFail` or `ReasonBan`: stop automatic reconnect and repair credentials or policy state.
* `ReasonClientKeyIsEmpty` or `ReasonProtocolUpgradeRequired`: fix client configuration or version first.
* `ReasonRateLimit`, `ReasonSystemError`, or transport loss: use only bounded backoff with jitter; rediscover Gateway ingress when needed.
* Any other failure: preserve the raw Reason Code and fail closed; do not guess that it is retryable.
After close, stop new sends. Reuse a stable `client_msg_no` for the same product send and distinct `client_seq` values for concurrent wire attempts. Return to ready only after CONNACK succeeds and durable-message recovery completes.
The current default composition enables Session payload encryption and stored device-token validation. CONNECT without `client_key` returns `ReasonClientKeyIsEmpty`; an empty token, missing device record, or mismatch returns `ReasonAuthFail`. Exact token matching does not replace TLS, account login, credential-expiry or replay policy, or WuKongIM HTTP API protection.
Continue with [Packet Types](/en/api/client-protocols/packet-types) and [Reason Codes](/en/api/dictionaries/reason-codes).
# Encryption & Security (/en/api/client-protocols/encryption)
The current default composition enables WKProto session Payload encryption. This is a client-compatibility algorithm that protects SEND/RECV Payloads only; CONNECT, headers, and routing metadata remain outside this layer.
## Handshake and keys [#handshake-and-keys]
1. The client generates an ephemeral X25519 key pair and sends the standard Base64 encoding of its 32-byte public key as CONNECT `client_key`.
2. The server generates an ephemeral X25519 pair, computes the shared secret, and returns Base64 `server_key` plus `salt` in CONNACK.
3. Both sides calculate `shared = X25519(private, peer_public)`.
4. `AES key = lowercase_hex(MD5(Base64(shared)))[0:16]`; those 16 ASCII bytes are the AES-128 key.
5. `salt` is 16 random alphanumeric ASCII bytes used as the IV and reused for the session.
When encryption is enabled, a missing `client_key` returns `ReasonClientKeyIsEmpty`.
## Payload [#payload]
```text
wire_payload = Base64(AES-128-CBC(PKCS#7(plain_payload), key, iv))
```
For SEND, the server validates `msg_key` before decrypting the Payload. For RECV, it encrypts the Payload before calculating `msg_key`. `SettingNoEncrypt (0x10)` bypasses both steps for that packet.
## msg\_key [#msg_key]
Convert numeric fields to decimal text and concatenate fields in this exact order **without delimiters**. `encrypted_payload` is the Base64 Payload bytes carried on the wire.
| Packet | Verification bytes |
| ------ | ----------------------------------------------------------------------------------------------------------------- |
| SEND | `client_seq + client_msg_no + channel_id + channel_type + encrypted_payload` |
| RECV | `message_id + message_seq + client_msg_no + timestamp + from_uid + channel_id + channel_type + encrypted_payload` |
Then calculate:
```text
msg_key = lowercase_hex(MD5(Base64(AES-128-CBC(PKCS#7(verification_bytes), key, iv))))
```
The server validates SEND `msg_key`. The repository Go tooling client decrypts RECV but currently **does not validate its `msg_key`**; verify the behavior of each other client implementation separately.
## Security boundary [#security-boundary]
This compatibility design does not authenticate the X25519 peer, uses CBC with a reused IV and an MD5 check, and is not AEAD. It does not replace TLS, token verification, identity authentication, or end-to-end encryption. The CONNECT token is sent before session keys exist and is observable without TLS.
Production deployments must use TLS/WSS or an equivalent trusted private boundary and enforce certificate, identity, and token verification at ingress. Do not set `SettingNoEncrypt` for sensitive messages.
# Client Protocols (/en/api/client-protocols)
Clients establish long-lived connections through Gateway TCP or WebSocket listeners. The WKProto baseline covers negotiation, send, receive acknowledgement and heartbeat. MQTT uses a separate MQTT 5 TCP entry while sharing IM users, channels and messages.
TCP binary frames and WebSocket messages do not belong in OpenAPI. This section records wire and Gateway behavior without inventing HTTP paths.
## Current boundary [#current-boundary]
* The public core contains CONNECT/CONNACK, SEND/SENDACK, RECV/RECVACK, PING/PONG, and outbound server stream.\* EVENT.
* WKProto `DISCONNECT`, `SUB`, and `SUBACK` are currently codec contracts, not published product Gateway entry points.
* The server can deliver public streaming message events through `EVENT`. Arbitrary client EVENT ingress remains unsupported; ingress accepts only benchmark terminal-fence tooling requests.
* WebSocket can carry WKProto bytes. The JSON-RPC bridge supports the pinned EasySDK core online path; the matrix marks every other method experimental or missing.
See [Shared Dictionaries](/en/api/dictionaries) for numeric meanings and [Authentication & Security](/en/api/authentication) for the production trust boundary.
# WebSocket JSON-RPC (/en/api/client-protocols/json-rpc)
Product Gateway supports the pinned EasySDK CONNECT-first authentication lifecycle, ping, online SEND/SENDACK, RECV/RECVACK, and server-to-client public stream.\* EVENT delivery. General-purpose JSON-RPC, batches, subscriptions, and automatic SDK offline sync remain outside this contract.
## wsmux selection [#wsmux-selection]
WebSocket `wsmux` selects JSON-RPC when the first non-whitespace byte is `{` or `[`, and WKProto otherwise. The choice persists for the session. JSON-RPC output uses text messages; WKProto output uses binary messages.
`[` selects JSON mode, but the decoder does **not** implement batch arrays. `jsonrpc` may be omitted, and request IDs must be JSON strings. SEND `payload` may be an already-encoded JSON text string, a JSON object, a Base64 string, or `null`; the protocol boundary normalizes each form to bytes. A string whose decoded contents are valid JSON is treated as JSON text before the Base64 fallback. JSON-RPC does not perform WKProto `clientKey` negotiation, so production transport must use `wss://`.
## Actual inbound matrix [#actual-inbound-matrix]
| Kind | Method | Decode / bridge | Product result |
| ------------ | ------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| request | `connect` | → `CONNECT` | must be first; successful authentication and activation return same-ID `result`, failure returns same-ID `error` and closes |
| request | `send` | → `SEND` | handled only on an authenticated session; returns same-ID SENDACK result/error |
| request | `ping` | → `PING` | returns the same ID with explicit `result: null` |
| request | `disconnect` | → `DISCONNECT` | unsupported by the product handler; closes |
| request | `subscribe` / `unsubscribe` | → `SUB` (`action` is `0` / `1`) | the Product Gateway handler does not support `SUB`; the default handler-error policy closes the session, so the current product path cannot emit `SUBACK` |
| notification | `recvack` | → `RECVACK` | confirms online delivery on an authenticated session; iOS `v1.1.1` sends both camelCase `messageId` and `messageSeq` |
| notification | `recv` / `disconnect` / `event` | decoded only | frame bridge missing |
| response | any structurally valid response | decoded to a generic response | frame bridge missing |
Successful CONNACK and SENDACK responses include both camelCase and Android snake\_case fields; clients ignore the unused aliases. A non-success Reason Code becomes a JSON-RPC `error`, preventing EasySDK from mistaking failure for success. RECV is an ID-less `recv` notification that always contains `header: {}`. Valid JSON-object payload bytes are emitted as an object; non-object bytes fall back to Base64. The Android `v1.0.5` README example still passes its already-encoded JSON message as a text string, while the protocol boundary also accepts direct objects and Base64 strings; normal JSON message bytes are emitted on RECV as an object for all four pinned profiles.
## Online streaming events [#online-streaming-events]
Server `EVENT` becomes an ID-less `event` notification with `{id,type,timestamp,data}` parameters; valid JSON data is emitted as an object. EasySDK-JS receives public `stream.*` events through `WKIMEvent.CustomEvent`. This requires the server online event delivery implementation and is separate from older core-message acceptance. Events are best effort without RECVACK replay; final snapshots recover through message history. See [Agent Streaming Replies](/en/sdk/easy/agent-streaming).
## Verified scope [#verified-scope]
Server fixtures cover the pinned wire profiles retained by the current iOS `v1.1.1`, Android `v1.0.5`, Flutter `v1.1.0`, and Web `v2.0.4` releases; these patch versions do not change the tested JSON-RPC shapes. A real `cmd/wukongim` 256-slot single-node cluster scenario uses the iOS and Android profiles to cover the Alice/Bob bidirectional CONNECT → SEND → SENDACK → RECV → RECVACK → ping loop, online-presence cleanup after disconnect, and reconnect. A separate released-package acceptance resolves those versions from all four registries and completes bidirectional messaging on Android and iOS Simulators. The default `gateway.token_auth_on=true` performs exact stored-device-token matching; production acceptance must still separately prove rejection of wrong, missing, revoked, and product-expired tokens.
Use [WuKongEasySDK](/en/sdk/easy) for lightweight online messaging. Choose a WKProto SDK and the [TCP binary protocol](/en/api/client-protocols/tcp-binary) when you need a local message store, conversations, unread state, offline recovery, or broader capabilities.
# Packet Types (/en/api/client-protocols/packet-types)
`FrameType` occupies the high four bits of the fixed header. `UNKNOWN=0` is reserved and must not be sent. Enum presence does not mean the product Gateway supports that packet.
## Core fields [#core-fields]
| Packet | Body fields |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `CONNECT` | `version`, `client_key`, `device_id`, `device_flag`, millisecond timestamp, `uid`, `token` |
| `CONNACK` | negotiated version for v4+ requests, `server_key`, `salt`, time difference, Reason Code, v4+ `node_id` |
| `SEND` | Setting, 32-bit wire `client_seq`, `client_msg_no`, Channel, v3+ `expire`, `msg_key`, optional Topic, Payload |
| `SENDACK` | `message_id`, `client_seq`, `message_seq`, Reason Code, optional `client_msg_no` |
| `RECV` | Setting, `msg_key`, sender, Channel, v3+ `expire`, `client_msg_no`, `message_id`, `message_seq`, second timestamp, optional Topic, Payload |
| `RECVACK` | `message_id`, `message_seq` |
| `PING` / `PONG` | No body; the complete packet is one fixed-header byte |
Protocol v5 and earlier use a 32-bit `message_seq`; v6 uses 64 bits. The wire width of `client_seq` is always 32 bits. The current encoder limits SEND Payload to 32,767 bytes; the decoder rejects frames whose remaining length exceeds 1 MiB.
See [Message Flags](/en/api/dictionaries/message-flags) for fixed-header and Setting bits, [Device Flags](/en/api/dictionaries/device-flags) for device values, [Channel Types](/en/api/dictionaries/channel-types) for Channel values, and [Reason Codes](/en/api/dictionaries/reason-codes) for outcomes.
The current product Gateway accepts no ordinary inbound `DISCONNECT`, `SUB`, or arbitrary `EVENT`; client `EVENT` ingress is only for benchmark terminal-fence; server output also delivers public streaming message events. `UNKNOWN=0` never reaches product handling. Unknown numeric types, malformed frames, and decoded-but-unpublished packets fail closed through protocol-error, handler-error, or bounded-timeout paths.
## WKProto packet catalog
| Value | Name | Direction | Scope | Meaning |
| --- | --- | --- | --- | --- |
| 0 | `UNKNOWN` | — | Reserved | Wire-reserved; do not send; it never reaches product handling. |
| 1 | `CONNECT` | Client → Server | Public core | Must be the sole first packet on a connection. |
| 2 | `CONNACK` | Server → Client | Public core | Returns connection result and session material; v4+ requests also receive the negotiated version. |
| 3 | `SEND` | Client → Server | Public core | Submits one Channel message. |
| 4 | `SENDACK` | Server → Client | Public core | Correlates a SEND with its protocol result. |
| 5 | `RECV` | Server → Client | Public core | Delivers one message to the current Session. |
| 6 | `RECVACK` | Client → Server | Public core | Acknowledges client handling of a specific RECV. |
| 7 | `PING` | Client → Server | Public core | Refreshes inbound activity and requests PONG. |
| 8 | `PONG` | Server → Client | Public core | Bodyless response to PING. |
| 9 | `DISCONNECT` | Client ↔ Server | Codec only | Bidirectional codec contract; the current product Gateway publishes neither client ingress nor server emission. |
| 10 | `SUB` | Client → Server | Codec only | Codec exists; current product Gateway ingress does not support it. |
| 11 | `SUBACK` | Server → Client | Codec only | Codec response for SUB; current product entry is unpublished. |
| 12 | `EVENT` | Client ↔ Server | Public core | The server delivers public stream.* message events to clients; client ingress accepts only benchmark terminal-fence tooling requests. |
Protocol v6 uses a 64-bit `message_seq`; v5 and earlier use 32 bits. The wire width of `client_seq` is always 32 bits.
# TCP Binary Protocol (/en/api/client-protocols/tcp-binary)
WKProto runs over a TCP byte stream. WKProto over WebSocket carries the same bytes. The table below is the current encoder's **exact body order**, not an HTTP OpenAPI surface.
## Frame boundary [#frame-boundary]
```text
ordinary frame = fixed_header:u8 + remaining_length:varuint + body
PING/PONG = fixed_header:u8
```
* Bits 7..4 of `fixed_header` contain `FrameType`.
* Except for `CONNACK`, bits 3..0 are `DUP`, `SyncOnce`, `RedDot`, and `NoPersist`.
* The `CONNACK` encoder uses only bit 0 for `HasServerVersion`.
* `remaining_length` is the body length encoded base-128, least-significant 7-bit group first; `0x80` means another byte follows.
* Multibyte scalars are big-endian. `str16be` is an `i16be` byte length followed by raw bytes, with a 32767-byte maximum. `bytes-rest` consumes the rest of the body.
* The decoder rejects `remaining_length > 1 MiB`; the SEND encoder rejects a Payload over 32767 bytes.
## WebSocket carrier [#websocket-carrier]
* Upgrade requires WebSocket v13, `GET`, and the listener's exact configured path; an empty path means `/`.
* Client frames must be masked and server frames are unmasked. Fragmented Text/Binary messages are reassembled under the default 1 MiB session inbound limit.
* WKProto uses Binary messages. WebSocket control `PING/PONG` is answered by the carrier and neither replaces nor reaches the WKProto `PING/PONG` heartbeat.
## Body layouts [#body-layouts]
`v` is the negotiated version, `S` is Setting, and `H0` is fixed-header bit 0.
| Value | Packet / direction | Body fields, left to right |
| ----: | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0 | `UNKNOWN` / reserved | none; never send |
| 1 | `CONNECT` C→S | `version:u8`, `device_flag:u8`, `device_id:str16be`, `uid:str16be`, `token:str16be`, `client_timestamp:i64be(ms)`, `client_key:str16be` |
| 2 | `CONNACK` S→C | `[server_version:u8 if H0]`, `time_diff:i64be(ms)`, `reason_code:u8`, `server_key:str16be`, `salt:str16be`, `[node_id:u64be if v>=4]` |
| 3 | `SEND` C→S | `setting:u8`, `client_seq:u32be`, `client_msg_no:str16be`, `[stream_no:str16be if 2<=v<5 and S.Stream]`, `channel_id:str16be`, `channel_type:u8`, `[expire:u32be if v>=3]`, `msg_key:str16be`, `[topic:str16be if S.Topic]`, `payload:bytes-rest` |
| 4 | `SENDACK` S→C | `message_id:i64be`, `client_seq:u32be`, `message_seq:seq(v)`, `reason_code:u8`, `[client_msg_no:str16be if non-empty]` |
| 5 | `RECV` S→C | `setting:u8`, `msg_key:str16be`, `from_uid:str16be`, `channel_id:str16be`, `channel_type:u8`, `[expire:u32be if v>=3]`, `client_msg_no:str16be`, `[stream_flag:u8, stream_no:str16be, stream_id:u64be if 2<=v<5 and S.Stream]`, `message_id:i64be`, `message_seq:seq(v)`, `timestamp:i32be(s)`, `[topic:str16be if S.Topic]`, `payload:bytes-rest` |
| 6 | `RECVACK` C→S | `message_id:i64be`, `message_seq:seq(v)` |
| 7 | `PING` C→S | no body and no `remaining_length` |
| 8 | `PONG` S→C | no body and no `remaining_length` |
| 9 | `DISCONNECT` bidirectional / codec-only | `reason_code:u8`, `reason:str16be` |
| 10 | `SUB` C→S / codec-only | `setting:u8`, `sub_no:str16be`, `channel_id:str16be`, `channel_type:u8`, `action:u8`, `param:str16be` |
| 11 | `SUBACK` S→C / codec-only | `sub_no:str16be`, `channel_id:str16be`, `channel_type:u8`, `action:u8`, `reason_code:u8` |
| 12 | `EVENT` bidirectional / outbound stream.\* | `id:str16be`, `type:str16be`, `timestamp:i64be`, `data:bytes-rest` |
`seq(v)` is `u32be` through v5 and `u64be` in v6. The current SENDACK decoder also accepts the compatibility order `message_id, client_seq, client_msg_no, message_seq, reason_code`; new encoders use the primary order above.
## Version and Setting [#version-and-setting]
* The current server version is v6. CONNECT `version=0` or a value above 6 selects v6; values 1–6 remain unchanged.
* When the requested version is above 3, CONNACK sets `H0` and includes `server_version`.
* Setting bits are `Receipt=0x80`, `Signal=0x20`, `NoEncrypt=0x10`, `Topic=0x08`, and `Stream=0x02`.
The public product Gateway accepts only `PING`, `SEND`, and `RECVACK` after open; `CONNECT` must be the sole first packet and is handled during connection setup. Ordinary `DISCONNECT`, `SUB`, and `SUBACK` input is unsupported. Client `EVENT` ingress is reserved for benchmark terminal-fence tooling; server `EVENT` output also delivers public streaming message events.
See [Connection Lifecycle](/en/api/client-protocols/connection-lifecycle) for session state and heartbeat, and [Shared Dictionaries](/en/api/dictionaries) for numeric meanings.
# Channel Type (/en/api/dictionaries/channel-types)
`channel_id + channel_type` jointly identifies one Channel. The numeric value crosses protocol, HTTP, storage, and routing contracts. Integrators transmit the number and retain the name as readable metadata.
## Goal and completion criteria [#goal-and-completion-criteria]
After choosing a Channel Type, you can name its membership model, message recovery, conversation projection, and source of product authorization. “Present in the enum” does not mean the current SDK or public API publishes a complete integration path.
## Authority [#authority]
The table aligns with current `ChannelType*` constants 1–12 in `pkg/protocol/frame/common.go`; tests check names and values. It belongs to the current source snapshot and is not a promise across arbitrary versions.
## Integrator defaults [#integrator-defaults]
* Start one-to-one communication with `ChannelTypePerson=1`; callers use the peer UID.
* Start ordinary group chat with `ChannelTypeGroup=2`; the product service first owns group ID, membership, and policy.
* Use community, information, live, visitor, or Agent types only when product requirements and the selected client explicitly cover them.
* New visitor-support flows should not select `ChannelTypeCustomerService=3`, which source marks as deprecated.
* A temporary Channel is not a stable product group ID.
Changing `channel_type` for one product Channel creates a different routing and storage identity. Treat it as an explicit data and client-compatibility migration, not a configuration edit.
## Direct-chat identity [#direct-chat-identity]
When Alice sends to Bob, the caller uses Bob's UID. The server entry normalizes both UIDs into an internal person-Channel identity; responses or webhooks may expose that canonical value. Product friendship still uses the two UIDs and never writes the internal value back as “peer UID.”
## Specialized-type checklist [#specialized-type-checklist]
Before using a specialized type, verify:
1. the current server path provides the required membership, permission, persistence, and conversation semantics;
2. the exact SDK version encodes, decodes, and presents the type correctly;
3. offline sync, push, callbacks, and historical migration have coverage;
4. a 100,000-member or high-rate workload uses bounded membership mutation and paged fanout;
5. old clients that do not recognize the type degrade safely.
## Troubleshooting [#troubleshooting]
* **ReasonNotSupportChannelType:** confirm protocol version and value; do not blindly retry as another type.
* **Direct history splits:** inspect using both peer UID and canonical person ID as caller input.
* **Group send is denied:** inspect membership, allow/deny lists, ban, and disband instead of switching to Person to bypass policy.
* **Old client cannot render:** preserve server data and degrade through payload/client compatibility policy.
## Security boundary [#security-boundary]
Channel Type is not authorization. Even with the correct type, the product service owns relationship creation, membership, roles, content governance, and tenant isolation.
## Next step [#next-step]
Use [Device Flags](/en/api/dictionaries/device-flags) for client identity and [Message Flags](/en/api/dictionaries/message-flags) for message behavior bits.
## Channel Types (shared contract)
| Value | Name | Scope | Integrator guidance |
| --- | --- | --- | --- |
| 1 | `ChannelTypePerson` | Integration baseline | Direct chat; callers use the peer UID and the server entry normalizes the two-UID Channel identity. |
| 2 | `ChannelTypeGroup` | Integration baseline | Group chat; the product service must first maintain a stable Channel ID, membership, and send policy. |
| 3 | `ChannelTypeCustomerService` | Compatibility / legacy | Legacy customer-service type; source marks it deprecated in favor of ChannelTypeVisitors for new visitor flows. |
| 4 | `ChannelTypeCommunity` | Specialized | Community container type; use only when the selected SDK and product flow explicitly support it. |
| 5 | `ChannelTypeCommunityTopic` | Specialized | Community-topic type; do not interchange it with a community container or ordinary group chat. |
| 6 | `ChannelTypeInfo` | Specialized | Information Channel with temporary-subscriber semantics; verify the matching membership lifecycle before integration. |
| 7 | `ChannelTypeData` | Specialized | Data Channel; enum presence does not mean a complete platform integration flow is published. |
| 8 | `ChannelTypeTemp` | Specialized | Temporary or request-scoped target Channel; do not persist it as a product group ID. |
| 9 | `ChannelTypeLive` | Specialized | Live Channel; current semantics do not retain recent-conversation data. |
| 10 | `ChannelTypeVisitors` | Specialized | Visitor Channel; the Channel ID is the visitor UID and may represent one visitor with multiple support subscribers. |
| 11 | `ChannelTypeAgent` | Specialized | Direct Agent Channel; its internal identity is shaped like UID@AgentID while the product service still owns Agent authorization. |
| 12 | `ChannelTypeAgentGroup` | Specialized | Group Agent Channel for multi-Agent collaboration; it is not a transparent alias for ordinary group chat. |
# Device Flag (/en/api/dictionaries/device-flags)
Device Flag identifies a client device category. Device Level expresses connection-conflict policy within the same UID and category. Neither is an OS version, concrete device ID, or product role.
## Goal and completion criteria [#goal-and-completion-criteria]
The product backend chooses a stable `device_flag`, concrete `device_id`, and server-controlled `device_level` for every endpoint and can explain whether same-category devices coexist or use single-active policy.
## Authority [#authority]
The tables align with `internal/contracts/protocolmeta/types.go` and `pkg/protocol/frame/common.go`. Tests freeze client categories, the server-reserved value, and both conflict levels.
## Do not merge these fields [#do-not-merge-these-fields]
| Field | Question answered |
| -------------- | ------------------------------------------------------------------------------------ |
| `device_flag` | Is this APP, WEB, PC, or server-side SYSTEM? |
| `device_id` | Which concrete installation or endpoint in that category? |
| `device_level` | May endpoints in the same UID/category coexist or use single-active conflict policy? |
Do not use a phone brand, browser user agent, or OS version directly as `device_id`. Generate a stable random identifier and define whether reinstall, cleared storage, device migration, or backup restore creates a new ID.
## Current authentication behavior [#current-authentication-behavior]
`gateway.token_auth_on` defaults to `true`. Gateway reads the stored device token by UID and Device Flag, and the Session adopts that record's Device Level only after the CONNECT token matches exactly. An empty token, missing or cleared device record, or mismatch returns `ReasonAuthFail`.
After a trusted backend writes `device_level=1`, a connection with the matching token therefore adopts Master policy. Before release, still test new Master connection, old Session closure, cross-node conflict, revocation, and reconnect rejection. Do not disable token authentication in production to bypass those checks.
## Integration guidance [#integration-guidance]
* Use different categories when APP, WEB, and PC coexist.
* Use Slave for multiple same-category endpoints and process RECVACK independently per Session.
* A backend issues Master for single-active policy; clients never select it.
* SYSTEM is for trusted server identities; reject it from end-user ingress.
* Keep provider push token, OS permissions, risk, and revocation in a product device registry; Presence is not a device database.
## Troubleshooting [#troubleshooting]
* **PC renders as a number:** wire value 2 remains authoritative; a log/management String method is not the numeric contract.
* **Same-category Master does not replace:** inspect the Device Level actually returned by the CONNECT verifier.
* **Different platforms replace each other:** inspect accidental reuse of one Device Flag.
* **A device reconnects after logout:** inspect token revocation and Gateway verification rather than Session close alone.
## Security boundary [#security-boundary]
A device category grants no product permission. SYSTEM is not an “administrator mode” for ordinary clients; restrict it to trusted internal components and reject spoofing at public ingress.
## Next step [#next-step]
Apply these values with the [Device](/en/guide/core-concepts/devices) model, then use [Authentication & Security](/en/guide/integration/authentication) for issuance and revocation.
## Device Flags (shared contract)
| Value | Name | Scope | Integrator guidance |
| --- | --- | --- | --- |
| 0 | `APP` | Client | Native mobile-application device category. |
| 1 | `WEB` | Client | Web browser or web-application device category. |
| 2 | `PC` | Client | Desktop-client device category. |
| 99 | `SYSTEM` | Server-reserved | Server-reserved system device category; end-user clients must not impersonate it. |
### Device Levels
| Value | Name | Scope | Integrator guidance |
| --- | --- | --- | --- |
| 0 | `DeviceLevelSlave` | Client | Allows multiple endpoints in the same device category to coexist. |
| 1 | `DeviceLevelMaster` | Client | Declares single-active conflict policy within one device category; enforcement still depends on verified connection authentication and composition. |
# Shared Dictionaries (/en/api/dictionaries)
## Goal and completion criteria [#goal-and-completion-criteria]
This area centralizes wire values reused across client SDKs, Gateway, and compatible HTTP results. Phase 13 adds Channel Type, Device Flag / Level, and Message Flags to Reason Code so integrators can cross-check numeric values and behavior across languages.
## Compatibility target [#compatibility-target]
Every table aligns with current authoritative Go definitions, and tests check names, values, or bit order. They belong to the current source snapshot. The [compatibility snapshot](/en/api/compatibility) still attests only the JavaScript golden scenario, not every SDK platform.
## Prerequisites [#prerequisites]
* Numeric values are the wire contract; names support readable diagnosis.
* Never interpret “present in the enum” as “the current SDK publishes a complete capability.”
* Preserve unknown raw values and fail closed.
## Published dictionaries [#published-dictionaries]
## Expected result [#expected-result]
Match by numeric value and retain dictionary name, protocol phase, and compatibility snapshot. Application code avoids scattered magic numbers and maps through SDK types or one internal shared module.
## Troubleshooting [#troubleshooting]
* **Name and value disagree:** fail validation and realign it; do not maintain a second handwritten table.
* **A dictionary names a feature but the SDK has no entry:** that platform capability may remain unpublished; check exact version docs.
* **A value is absent from the table:** record the raw value and compatibility snapshot and remain fail closed until the dictionary is upgraded.
## Security and responsibility boundary [#security-and-responsibility-boundary]
A wire dictionary is neither authorization nor localized text safe for direct display. Map values to product-safe meaning without exposing topology, authorization policy, or sensitive request content.
## Next step [#next-step]
Start with [Channel Types](/en/api/dictionaries/channel-types) and [Device Flags](/en/api/dictionaries/device-flags), then use [Message Flags](/en/api/dictionaries/message-flags) and [Reason Codes](/en/api/dictionaries/reason-codes) to interpret message outcomes.
# Message Flags (/en/api/dictionaries/message-flags)
Two different bit sets affect a message: low fixed-header booleans and message `Setting` bits. Integrators use SDK-exposed types instead of hand-written magic numbers; this page cross-checks wire meaning across languages.
## Goal and completion criteria [#goal-and-completion-criteria]
For any flag, you can explain whether it affects persistence, routing, display, or encoding and avoid interpreting protocol intent as proof of business completion.
## Authority [#authority]
Fixed-header bits align with `pkg/protocol/codec/common.go`; Setting values align with `pkg/protocol/frame/setting.go`. Tests freeze bit order, names, and values.
## `NoPersist` delivers online without history [#nopersist-delivers-online-without-history]
Ordinary `NoPersist=true, SyncOnce=false` messages use the source Channel;
command-style messages use the command Channel. Both pass permission checks,
resolve authority, allocate a message ID, and enter transient online delivery
with sequence zero. They do not write durable history or support offline recovery.
* Reliable chat, notifications, audit, and replayable events use durable messages.
* Online-only messages use `NoPersist`; commands additionally use command semantics.
* Recoverable commands use durable `SyncOnce` with separate CMD bind/sync/ack.
* A successful transient SENDACK confirms acceptance into online delivery, not receipt by a device.
## `RedDot` and receipts [#reddot-and-receipts]
`RedDot` carries client display intent through storage, delivery, and compatibility callbacks, but it does not mean:
* the server changed a Conversation `read_seq`;
* the end user viewed the message;
* the receiver emitted a product receipt.
`SettingReceiptEnabled` is also protocol receipt intent. SENDACK is a Channel commit result, RECVACK is Session transport feedback, and end-user read or device execution needs a separate product contract.
## Encryption-related bits [#encryption-related-bits]
* `SettingNoEncrypt` makes compatible WKProto adapters skip negotiated Session payload encryption. It neither disables nor supplies TLS.
* Never enable it for sensitive messages.
* `SettingSignal` is specialized signal mode and requires explicit support from the exact SDK and protocol version.
* Do not invent an end-to-end encryption guarantee from one bit; key distribution, identity verification, and rotation require a complete design.
## Topic and Stream [#topic-and-stream]
`SettingTopic` indicates that the packet carries a Topic field. `SettingStream` indicates compatible stream fields; it does not automatically promise realtime token push, durable event projection, and reconnect recovery together. The current AI stream tutorial uses a durable base plus message-event projection and explicitly separates a realtime delta path.
## Combination guide [#combination-guide]
| Requirement | Selection |
| ---------------------------- | -------------------------------------------------------------------------------------- |
| Ordinary recoverable chat | `NoPersist=false`, `SyncOnce=false` |
| Ordinary online-only message | `NoPersist=true`, `SyncOnce=false`; accept no history |
| Online-only command | `NoPersist=true`, `SyncOnce=true`; accept no history |
| Recoverable command | `NoPersist=false`, `SyncOnce=true`; add CMD binding and sync |
| Product read receipt | Separate durable receipt; do not rely on `RedDot` or RECVACK |
| Streaming AI | Read [AI & IoT](/en/guide/tutorials/ai-and-iot) instead of setting only the Stream bit |
## Troubleshooting [#troubleshooting]
* **NoPersist succeeds but peer receives nothing:** check subscriber membership, online presence, delivery admission, and owner Session writes. Offline recipients cannot recover the message.
* **Message cannot recover offline:** inspect NoPersist or a missing CMD recovery flow.
* **Red dot and unread disagree:** inspect Message Flag separately from Conversation badge floor.
* **One SDK fails after a Setting bit is enabled:** verify exact SDK and protocol version rather than retaining an unknown bit blindly.
## Security boundary [#security-boundary]
Unknown bits fail closed or are ignored safely by a compatible SDK; they never default to enabled. Raw `setting` and Header may enter controlled diagnostics, but never write them together with a complete sensitive payload into ordinary logs.
## Next step [#next-step]
Apply these bits in [Messaging](/en/guide/integration/messaging), and classify outcomes through [Reason Codes](/en/api/dictionaries/reason-codes).
## Message flags (shared contract)
### Fixed-header bits
| Bit | Name | Scope | Integrator guidance |
| --- | --- | --- | --- |
| 0 | `NoPersist` | Wire flag | Ordinary and command-style messages enter transient online delivery with a message ID and sequence zero. Neither writes durable history or supports offline recovery. |
| 1 | `RedDot` | Wire flag | Carries red-dot display intent; it is not a read receipt and does not by itself prove a server unread-count change. |
| 2 | `SyncOnce` | Wire flag | Routes command-style messages through a separate CMD Channel; recoverable commands additionally require binding and CMD synchronization. |
| 3 | `DUP` | Wire flag | Protocol retransmission marker; product idempotency still relies on a stable client_msg_no and result correlation. |
### Setting bit values
| Value | Name | Scope | Integrator guidance |
| --- | --- | --- | --- |
| 128 | `SettingReceiptEnabled` | Wire flag | Enables protocol receipt intent; do not equate it with Channel commit, device-side business execution, or end-user read state. |
| 32 | `SettingSignal` | Wire flag | Marks compatible signal mode; use only when the selected SDK and protocol version explicitly support it. |
| 16 | `SettingNoEncrypt` | Wire flag | Skips negotiated session payload encryption; it does not replace TLS and should not be enabled for sensitive messages. |
| 8 | `SettingTopic` | Wire flag | Indicates that the packet carries a Topic field; Topic lifecycle remains a compatible-client and product contract. |
| 2 | `SettingStream` | Wire flag | Indicates compatible stream-message fields; durable AI stream projection and realtime deltas remain separate paths. |
# Reason Code (/en/api/dictionaries/reason-codes)
Business send rejections additionally reserve 128–255. `msg.before_send` returns these unchanged through SENDACK `reason_code` and WuKongIM HTTP API `reason`. They are separate from the system enum below; SDK/application code should preserve the value, provide business copy, and avoid automatically retrying explicit rejection.
## Goal and completion criteria [#goal-and-completion-criteria]
This page maps the complete current `pkg/protocol/frame.ReasonCode` enum to developer decisions. When finished, you should preserve the raw value, interpret it in a CONNACK, SENDACK, or other phase, and apply bounded retries that do not amplify failure.
## Compatibility target [#compatibility-target]
The table is generated from the authoritative Go enum and shared metadata, and CI verifies values `0–29` and their names. It is tied to the [current API compatibility snapshot](/en/api/compatibility), with no separate handwritten copy in each MDX locale.
## Prerequisites [#prerequisites]
* Retain the packet type that carried the value instead of a bare number;
* distinguish HTTP status, CONNACK, SENDACK, online delivery, and synchronization;
* implement retries with idempotent identity, backoff, a cap, and cancellation;
* redact identity and message content from logs and metrics.
## Read the table [#read-the-table]
* **Phase** identifies whether the value belongs to CONNECT/CONNACK, SEND/SENDACK, disconnect, or a compatible-path context.
* **Retry** is a safe default, not permission for endless retries. Combine it with idempotent identity, backoff, a cap, and cancellation.
* **Reachability** distinguishes values emitted by a current production path, values requiring a specific configuration/state, and compatible enum values not evidenced by the current golden path.
* Presence in the enum does not mean every endpoint or SDK call can return the value.
## Client processing order [#client-processing-order]
1. Record packet type, numeric Reason Code, compatibility snapshot, and request correlation first.
2. Only explicit Success completes the current protocol phase.
3. Stop blind retries for identity, authorization, denylist/allowlist, ban, disband, invalid-field, or unsupported outcomes.
4. Retry route movement, rate limit, and system errors only with bounded exponential backoff, rediscovering required state first.
5. Fail closed on an unknown numeric value. Preserve it and never map it to Success.
## Send outcome and message stages [#send-outcome-and-message-stages]
For a persistent SEND, a successful SENDACK means the Channel authority made a successful durable-commit decision. It does not prove online receipt, RECVACK, webhook, plugin, offline synchronization, or product action.
A successful CONNACK means the current Gateway connection stage completed. With the default `gateway.token_auth_on=true`, it also means the CONNECT token matched the stored token for the same UID and device category. It does not prove account login, product authorization, credential expiry, or replay policy.
## Expected result [#expected-result]
The golden sample records SENDACK success/failure and message sequence. Failure text preserves the raw Reason Code value so it can be interpreted with this table. The event log does not display token or Payload; a product UI maps its own localized product copy.
## Troubleshooting [#troubleshooting]
* **Value and name disagree:** generation or version has drifted, and validation must fail.
* **A value marked unverified in the golden path appears:** preserve the raw evidence and source revision; do not rewrite reachability to accommodate one observation.
* **A retryable result repeats:** stop at the retry budget and inspect routing, rate limits, queue pressure, and cluster state.
* **An authorization result persists:** correct the authoritative product account, membership, allowlist/denylist, or Channel state.
* **The value is unknown:** treat it as failure and update the compatibility manifest and dictionary alignment.
## Security and responsibility boundary [#security-and-responsibility-boundary]
Reason Code handling must not disclose topology, risk decisions, or sensitive identity. Keep metric fields low-cardinality and exclude UID, token, Channel ID, and message body from metric labels. End-user language belongs to the product.
## Next step [#next-step]
Return to [Error Responses](/en/api/product-http/errors) to combine HTTP and protocol handling, or observe the difference between SENDACK and synchronization in the [Quickstart](/en/sdk/javascript/quickstart).
## Complete wire ReasonCode enum (shared contract)
| Value | Name | Stage | Retry guidance | Reachability | Meaning |
| --- | --- | --- | --- | --- | --- |
| 0 | `ReasonUnknown` | CONNECT / SEND | application-policy | compatibility | Unclassified or unknown result; retain context and avoid blind retries. |
| 1 | `ReasonSuccess` | CONNECT / SEND | not-applicable | active | The request succeeded; the completion boundary depends on the packet type. |
| 2 | `ReasonAuthFail` | CONNECT / SEND | do-not-retry | active | Authentication or sender identity was rejected; repair credentials or server policy first. |
| 3 | `ReasonSubscriberNotExist` | SEND | do-not-retry | active | The sender is absent from the channel membership required for this send. |
| 4 | `ReasonInBlacklist` | SEND | do-not-retry | active | The sender is present in the channel denylist. |
| 5 | `ReasonChannelNotExist` | SEND | application-policy | active | The channel does not exist or cannot currently accept the send. |
| 6 | `ReasonUserNotOnNode` | CONNECT / delivery | refresh-route | compatibility | The target user is not on this node; refresh routing before deciding whether to retry. |
| 7 | `ReasonSenderOffline` | SEND | application-policy | reserved | Reserved sender-offline result; no current product path emits this value. |
| 8 | `ReasonMsgKeyError` | SEND | do-not-retry | reserved | Reserved message-key or integrity error; no current product path emits this value. |
| 9 | `ReasonPayloadDecodeError` | SEND | do-not-retry | active | Malformed or unsupported SEND request (fields or payload), including decode failures. |
| 10 | `ReasonForwardSendPacketError` | SEND | retry-with-backoff | compatibility | A compatibility forwarding path could not forward the send packet. |
| 11 | `ReasonNotAllowSend` | SEND | do-not-retry | active | Channel policy does not allow this sender to send. |
| 12 | `ReasonConnectKick` | CONNECT / disconnect | application-policy | reserved | Reserved connection-kick result; no current product path emits this value. |
| 13 | `ReasonNotInWhitelist` | SEND | do-not-retry | active | The channel requires an allowlist and the sender is absent from it. |
| 14 | `ReasonQueryTokenError` | CONNECT | retry-with-backoff | reserved | Reserved token-query error; the current default composition does not emit this value. |
| 15 | `ReasonSystemError` | CONNECT / SEND | retry-with-backoff | active | Server pressure or internal error; retry with bounded backoff and the same idempotency key. |
| 16 | `ReasonChannelIDError` | SEND | do-not-retry | reserved | Reserved channel-identifier error; current product paths use other error mappings. |
| 17 | `ReasonNodeMatchError` | CONNECT / SEND | refresh-route | compatibility | Compatibility node matching failed; refresh routing. |
| 18 | `ReasonNodeNotMatch` | SEND | refresh-route | active | The current node is not the fresh authority target; refresh routing and reuse the original idempotency key. |
| 19 | `ReasonBan` | CONNECT / SEND | do-not-retry | active | The user connection or channel send is banned. |
| 20 | `ReasonNotSupportHeader` | CONNECT / SEND | do-not-retry | reserved | Reserved unsupported-header result; no current product path emits this value. |
| 21 | `ReasonClientKeyIsEmpty` | CONNECT | do-not-retry | active | A handshake that requires a client key did not provide one. |
| 22 | `ReasonRateLimit` | CONNECT / SEND | retry-with-backoff | compatibility | Rate-limit result retained by clients and workload tooling; the current default product path does not emit it. |
| 23 | `ReasonNotSupportChannelType` | SEND | do-not-retry | reserved | Reserved unsupported-channel-type result; no current product path emits this value. |
| 24 | `ReasonDisband` | SEND | do-not-retry | active | The channel is disbanded and cannot accept further sends. |
| 25 | `ReasonSendBan` | SEND | do-not-retry | active | The sender is banned from sending. |
| 26 | `ReasonChannelDeleting` | SEND | retry-with-backoff | reserved | Reserved channel-deleting result; no current product path emits this value. |
| 27 | `ReasonProtocolUpgradeRequired` | CONNECT | upgrade-client | compatibility | The gateway retains this connection-failure classification; the current default authenticator does not emit it. |
| 28 | `ReasonIdempotencyConflict` | SEND | do-not-retry | reserved | Reserved idempotency-conflict result; no current product path emits this value. |
| 29 | `ReasonMessageSeqExhausted` | SEND | do-not-retry | reserved | Reserved message-sequence-exhausted result; no current product path emits this value. |
| 30 | `ReasonSystemBusy` | SEND | retry-with-backoff | active | Bounded server admission is saturated (gateway send queue or permission-read admission) and the request was not accepted; the connection stays open, so retry with backoff and the same ClientMsgNo. |
# Health & Readiness (/en/api/operations-http/health-and-readiness)
| Endpoint | Success | Failure and meaning |
| -------------- | -------------------------------------------------------------- | --------------------------------------------------- |
| `GET /healthz` | `200 {"status":"ok"}` | Proves only that the HTTP process responds |
| `GET /readyz` | Default `200 {"ready":true}`, or the provider's readiness body | Returns `503` with the provider body when not ready |
Both routes are always registered and have no built-in authentication. Use `/healthz` for process restart probes and `/readyz` for load-balancer admission. Readiness still does not prove spare capacity, end-to-end delivery, backup recoverability, or the safety of a high-risk change.
```bash
curl --fail http://127.0.0.1:5001/healthz
curl --fail http://127.0.0.1:5001/readyz
```
Both remain observable during restore maintenance. Retain the raw `/readyz` failure body; never restore product traffic because `/healthz` returned 200.
# Operations HTTP (/en/api/operations-http)
Operations HTTP shares the API listener with WuKongIM HTTP API, but it is for operators, not business integrations.
[Download the Operations HTTP OpenAPI 3.1 contract](/contracts/operations-http.openapi.json)
| Method | Path | Condition | Stability |
| ------ | ------------------ | ----------------------------------------- | ---------------------------- |
| `GET` | `/healthz` | Always registered | Stable |
| `GET` | `/readyz` | Always registered | Stable |
| `ANY` | `/metrics` | Registered with a Prometheus handler | Stable Prometheus exposition |
| `GET` | `/top/v1/snapshot` | Always registered; 404 without a provider | Unstable |
These four endpoints have no built-in authentication. Expose them only to a controlled network; add TLS, access control, and limits at a reverse proxy, service mesh, or firewall.
# Prometheus Metrics (/en/api/operations-http/metrics)
`ANY /metrics` is registered only when the app wires a Prometheus handler; otherwise the route is absent and returns 404. Its response is Prometheus exposition, not JSON, and it is outside the WuKongIM HTTP API contract. Scrapers should use `GET`.
```yaml
scrape_configs:
- job_name: wukongim
static_configs:
- targets: ['127.0.0.1:5001']
```
The endpoint has no built-in authentication and remains readable during restore maintenance. Expose it only on a monitoring network; add TLS and authentication at the outer boundary when crossing an untrusted network. Base alerts on the same release and representative load, including latency percentiles, queues, rejection, drops, and node skew.
Metrics show trends and alerts; they do not alone prove readiness or safe topology mutation. Use [`/readyz`](/en/api/operations-http/health-and-readiness) for traffic admission.
# Snapshots & Gated Routes (/en/api/operations-http/read-only)
## Top snapshot [#top-snapshot]
`GET /top/v1/snapshot` returns node-local JSON with `version=top/v1` and `scope=local_node`. It neither depends on Prometheus nor fans out across the cluster.
| Query | Default | Constraint |
| -------- | ---------- | ---------------------------------------------------------------------------- |
| `window` | `10s` | Go duration, minimum `2s` |
| `view` | `overview` | `overview`, `runtime`, `traffic`, `channel`, `storage`, `delivery`, or `all` |
| `limit` | `20` | Positive integer; values above `100` clamp to `100` |
A missing provider returns 404, collector warm-up returns 503, and an invalid query returns 400. There is no built-in authentication, and the contract is unstable.
## Debug: 9 registrations [#debug-9-registrations]
All require `observability.debug_api_enable=true`. When `bench.api_token` is configured, every `/debug/*` request also needs that exact Bearer; with an empty token there is no built-in authentication.
| Method | Path | Extra condition |
| ------ | ------------------------------------ | -------------------------------------------------------- |
| `GET` | `/debug/config` | Config provider required |
| `GET` | `/debug/cluster` | Cluster provider required |
| `GET` | `/debug/diagnostics/trace/:trace_id` | Diagnostics store required |
| `GET` | `/debug/diagnostics/message` | Diagnostics store required |
| `GET` | `/debug/diagnostics/events` | Diagnostics store required |
| `GET` | `/debug/goroutines` | None |
| `GET` | `/debug/goroutines/summary` | Returns 404 without a registry provider |
| `ANY` | `/debug/pprof` | None |
| `ANY` | `/debug/pprof/*name` | Standard CPU, heap, goroutine, and other pprof subroutes |
## Bench: 12 registrations [#bench-12-registrations]
All require `bench.api_enable=true`. A non-empty `bench.api_token` protects every route with an exact Bearer; an empty token preserves unauthenticated compatibility. Restore maintenance rejects Bench requests.
| Method | Path |
| ------ | --------------------------------------- |
| `GET` | `/bench/v1/capabilities` |
| `GET` | `/bench/v1/capacity-target` |
| `GET` | `/bench/v1/snapshot` |
| `GET` | `/bench/v1/presence/snapshot` |
| `POST` | `/bench/v1/terminal-fence/prepare` |
| `GET` | `/bench/v1/channel-runtime/snapshot` |
| `POST` | `/bench/v1/channel-runtime/probe` |
| `POST` | `/bench/v1/channel-runtime/evict` |
| `POST` | `/bench/v1/users/tokens` |
| `POST` | `/bench/v1/channels` |
| `POST` | `/bench/v1/channels/subscribers` |
| `POST` | `/bench/v1/channels/subscribers/remove` |
`terminal-fence/prepare` additionally requires a Terminal Fence Controller and a non-empty token. Bench performs data preparation, runtime eviction, and terminal fencing; it is neither read-only nor a public integration contract.
`users/tokens` persists device tokens through the user use case and increments `accepted_users` only after successful writes. Without the user use case, its capability is false and requests return 501. Persistence failure returns 500; a durable prefix is not rolled back and can be upserted again with the same credentials. Lifecycle workers complete this preparation before CONNECT while Gateway Token authentication remains enabled.
# Stability & Private Boundaries (/en/api/operations-http/stability)
| Surface | Audience | Stability | Operations OpenAPI |
| --------------------- | ----------------- | ----------------------------- | --------------------------------------------- |
| `/healthz`, `/readyz` | Operator | Stable | Published |
| `/metrics` | Operator | Stable Prometheus format | Published; response is not JSON |
| `/top/v1/snapshot` | Operator | Unstable | Published without a stability promise |
| `/debug/*` | Operator | Unstable, disabled by default | No; internal inventory and exposure risk only |
| `/bench/v1/*` | Agent / benchmark | Unstable, disabled by default | No; benchmark-private contract only |
| 110 Manager routes | Operator | Private operations contract | Never merge with WuKongIM HTTP API |
| Node transport | Cluster internal | Unstable compatibility IDs | Never fake it as HTTP/OpenAPI |
| MCP / Agent CLI | Operator / Agent | Closed tool contract | Use tool or CLI tables, not OpenAPI |
With Manager authentication disabled, ordinary read and write groups do not install permission middleware, so most management mutations reach their handlers. Only backup writes, restore, and MCP administration explicitly fail closed; `/mcp` uses its separate MCP Bearer. Authentication-off is not read-only mode.
Manager reflects the caller Origin on every route except `/mcp` (or uses `*` without an Origin), so keep it on an isolated administration network and terminate TLS outside the process. With authentication enabled, Manager JWT plus each route's `cluster.*:r|w` permission applies. Restore requires exact `cluster.restore:w`; wildcard permission is insufficient.
These pages inventory source; they do not promote private or unstable entries into compatibility promises. Public backend integration stays under [WuKongIM HTTP API](/en/api/product-http), and realtime clients use [WKProto](/en/api/client-protocols).
# Error Responses (/en/api/product-http/errors)
Check network and HTTP status first, then parse the endpoint-specific schema. Do not depend on one error field or exact error text.
## HTTP handling [#http-handling]
| Result | Retry? | Action |
| ------------------------------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `400` | Endpoint-dependent | It may be an input error or a dependency/downstream failure; first decide whether that operation may already have written partial state |
| `503 maintenance` | Yes, with bounded backoff | Wait for restore maintenance and observe `/readyz` |
| Other `5xx` or network timeout | Conditional | Use exponential backoff, jitter, a cap, and cancellation |
| `2xx` with a schema mismatch | No | Stop processing and inspect version or contract drift |
Common error bodies:
```json
{"msg":"...","status":400}
{"error":"maintenance","message":"restore maintenance is active"}
```
Error text is not a stable machine contract. Branch on HTTP status and schema.
## HTTP 200 still requires a business-result check [#http-200-still-requires-a-business-result-check]
* `/message/send` requires checking `reason`; only a success Reason Code means the message was accepted.
* `/channel/messagesyncbatch` requires checking every `items[].error`; an item failure still uses HTTP 200.
* `/message/syncack` returns 200 when the handling process has no saved generation, but acknowledges nothing; `last_message_seq` is not used for the actual acknowledgement.
* Several legacy delete or sign-out entries return 200 when the target is absent. Treat this as “the desired state holds,” not proof that this request deleted a record.
## Partial writes and uncertain outcomes [#partial-writes-and-uncertain-outcomes]
`/channel`, subscriber reset, allow/deny-list set, and device sign-out with `device_flag=-1` execute in stages. If a 400, 5xx, disconnect, or timeout occurs partway through, earlier stages may already be committed. Do not blindly reverse the operation. Persist the product-side desired version, re-read observable state, and idempotently replay the desired state.
WuKongIM HTTP API currently has only the legacy full read for an ordinary allowlist; it has no symmetric readback for ordinary subscribers, the denylist, or temporary subscribers. For strict coordination, keep the product database as the desired-state source and use the protected Manager query for bounded verification.
## Protocol boundary [#protocol-boundary]
HTTP success does not prove CONNECT, SENDACK, realtime delivery, or read success. Handle CONNACK and SENDACK separately using their packet type and [Reason Code](/en/api/dictionaries/reason-codes); stop and report unknown values.
## Log safety [#log-safety]
* Record method, path, HTTP status, and a separate request ID.
* Do not log tokens, Authorization, cookies, UIDs, message bodies, or complete raw errors.
* Bound retry count, duration, and concurrency.
# WuKongIM HTTP API (/en/api/product-http)
WuKongIM HTTP API provides basic integration and trusted-management operations. The complete OpenAPI contract matches all 48 current `internal/access/api` registrations.
Each operation page explains the type, requiredness, default, range, and runtime compatibility behavior of every query parameter and JSON field.
OpenAPI: [complete contract](/contracts/product-http.openapi.json) · narrow profiles: [basic integration](/contracts/javascript-web-quickstart.openapi.json) · [message sending](/contracts/product-http-messaging.openapi.json) · [Channel and Conversation](/contracts/product-http-management.openapi.json)
WuKongIM HTTP API has no general business authentication. Restrict network reachability and provide authentication, authorization, limits, and audit at an API Gateway, service mesh, or product backend.
## Choose by task [#choose-by-task]
| Task | Recommended entry | Completion check |
| -------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------- |
| Issue a connection identity | `/user/token` → `/route` → SDK CONNECT | After the token write, CONNACK still decides connection success |
| Send from a backend | `/message/send` | Inspect `reason` after HTTP 200 |
| Recover after disconnect | `/channel/messagesync` or its batch form | Advance by `message_seq`; inspect each batch-item `error` |
| Reconcile group membership | `/channel`, subscriber add/remove | Persist the product desired version; verify and replay desired state after failure |
| Synchronize the Conversation directory | `/conversation/list` | Echo the cursor until `done=true` |
The complete contract is authoritative for all 48 runtime entries. The three narrow profiles are reviewed task projections with runnable examples. Generated pages merge those examples and expose caller, node scope, non-atomic writes, and post-200 business checks in an “Operation boundary” callout.
## Channel observability boundary [#channel-observability-boundary]
| State | WuKongIM HTTP API readback |
| --------------------- | ------------------------------------------------------ |
| Allowlist | `GET /channel/whitelist`, a legacy unbounded full list |
| Ordinary subscribers | None |
| Denylist | None |
| Temporary subscribers | None |
When WuKongIM HTTP API has no symmetric readback, do not treat one 200 response as reconciliation proof. Keep the product database as the desired-state source and use the protected, bounded Manager query when strict verification is required.
## What deprecated means [#what-deprecated-means]
`deprecated` marks a compatibility entry that still exists but should not be the default for a new integration: replace `/channel/info` with `/channel`, list set with remove-all plus bounded add, and `/conversation/sync` with `/conversation/list`. Batch routing, online status, Channel disband/removal, full allowlist read, and command-message sync/bind compatibility entries currently have no one-to-one replacement in WuKongIM HTTP API; use them only after reading their operation boundaries. Node-local system-UID cache routes are for directed operations and cannot replace durable mutations.
## API directory [#api-directory]
## Basic integration contract [#basic-integration-contract]
The snapshot above proves only the example scope of the three golden-path operations; it is not end-to-end acceptance for the complete contract. Realtime client send and receive still use the SDK and Gateway.
## JavaScript Web golden-path contract
This is a non-exhaustive v3 Beta subset. These three calls belong only in the trusted localhost BFF; the browser must not call the WuKongIM HTTP API directly.
- `POST /user/token`
- `GET /route`
- `POST /channel/messagesync`
[OpenAPI 3.1 subset](/contracts/javascript-web-quickstart.openapi.json)
# Specifications & Downloads (/en/api/specifications)
This section collects machine-readable contracts. They describe the current repository snapshot; they do not automatically prove compatibility across arbitrary server, SDK, and contract versions.
| Format | Use for | Do not use for |
| ------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------- |
| OpenAPI 3.1 | HTTP requests, responses, and webhook callbacks | WKProto, MCP, node transport, or Unix-socket RPC |
| JSON Schema 2020-12 | The pinned EasySDK core plus remaining experimental JSON-RPC shapes | General-purpose JSON-RPC or platform-runtime compatibility claims |
| Protocol pages | Binary layout, negotiation, stability, and trust boundaries | HTTP SDK generation |
Backends should start with the complete WuKongIM HTTP API contract. Choose a narrow profile only for its bounded scenario. Full SDK realtime clients use [WKProto](/en/api/client-protocols/tcp-binary); the four pinned EasySDKs may use the supported lightweight [WebSocket JSON-RPC core contract](/en/api/client-protocols/json-rpc).
# Experimental JSON-RPC Schema (/en/api/specifications/json-rpc-schema)
This schema records the WebSocket JSON-RPC boundary. Product Gateway supports CONNECT, ping, and online bidirectional messaging for the pinned EasySDKs; batches, subscriptions, offline sync, and general-purpose JSON-RPC remain experimental or unimplemented.
[Download JSON Schema 2020-12](/contracts/json-rpc.experimental.schema.json)
The schema's `x-wukongim-stability` is `experimental-easysdk-core-supported`. Current behavior is:
* `connect` is the first request and returns a correlated result/error after authentication and activation;
* `send` and `ping` correlate SENDACK/PONG through the request ID;
* `recv` notifications and `recvack` complete online-delivery acknowledgement;
* `subscribe` and `unsubscribe` map to `SUB` at the codec boundary, and `SUBACK` has a response mapping; the Product Gateway handler still rejects `SUB`, so the current product path cannot use them;
* `[` selects JSON mode, but batch arrays are unimplemented; request IDs must be strings, and SEND payload may be an object, Base64, or `null`.
Schema entries outside the EasySDK core path remain non-promises. See [WebSocket JSON-RPC](/en/api/client-protocols/json-rpc) for the full matrix, pinned versions, and evidence boundary.
# OpenAPI Contracts (/en/api/specifications/openapi)
Every file uses OpenAPI 3.1. Complete contracts describe the current source surface. Narrow profiles are bounded adoption subsets of the same snapshot, not the complete API.
## Complete contracts [#complete-contracts]
| Contract | Scope | Trust boundary | Download |
| ----------------- | ----------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------- |
| WuKongIM HTTP API | 48 current business HTTP operations | No built-in business authentication; trusted backends only | [JSON](/contracts/product-http.openapi.json) |
| Operations HTTP | `/healthz`, `/readyz`, `/metrics`, `/top/v1/snapshot` | No built-in authentication; operations network only | [JSON](/contracts/operations-http.openapi.json) |
| Webhooks | `msg.notify`, `msg.offline`, `user.onlinestatus` | Outbound POST JSON; unsigned and best effort | [JSON](/contracts/webhooks.openapi.json) |
The Webhook contract uses the OpenAPI `webhooks` object; it does not fake callbacks as inbound server `paths`. Prometheus `/metrics` remains exposition format rather than an ordinary JSON resource.
## Narrow profiles [#narrow-profiles]
| Profile | Operations | Use | Download |
| ------------------------- | ---------: | --------------------------------------------------- | --------------------------------------------------------- |
| JavaScript Web quickstart | 3 | Controlled Token, Route, and reconnect-sync example | [JSON](/contracts/javascript-web-quickstart.openapi.json) |
| Message sending | 1 | Ordinary persistent `POST /message/send` | [JSON](/contracts/product-http-messaging.openapi.json) |
| Channel & Conversation | 15 | Reviewed 10-Channel and 6-Conversation subset | [JSON](/contracts/product-http-management.openapi.json) |
A profile's end-to-end verification status does not extend to complete WuKongIM HTTP API. Before generating an SDK, pin the contract file, server revision, and outer authentication policy; rerun contract and business acceptance after upgrades.
# Protocol Changelog (/en/api/specifications/protocol-changelog)
## Current snapshot [#current-snapshot]
| Surface | Current contract |
| ------------------------------------------ | ----------------------------------------------------------------------- |
| WuKongIM HTTP API | Complete 48-operation OpenAPI plus narrow 3 / 1 / 15-operation profiles |
| Operations HTTP | 4-operation OpenAPI |
| Webhooks | 3 OpenAPI Webhooks |
| WKProto | Latest protocol version `6` |
| WebSocket JSON-RPC | `experimental-easysdk-core-supported` |
| Manager, Debug, Bench, MCP, node transport | Private or unstable inventory, not a public compatibility promise |
This is a current source snapshot, not proof of compatibility with arbitrary historical releases. Revalidate whenever the server, SDK, or contract artifact changes.
## WKProto v5 → v6 [#wkproto-v5--v6]
Version 6 widens the wire representation of `SENDACK.message_seq` and `RECV.message_seq` from 32 to 64 bits. Version 5 and earlier retain 32-bit encoding. The wire width of `client_seq` remains 32 bits. Decode field order and width from the negotiated protocol version, never from the apparent integer size.
Clients must negotiate through `CONNECT` / `CONNACK` and use that codec version for the whole session. A replay, proxy, or store that persists binary frames across versions must retain the protocol version with them.
## Breaking-change rules [#breaking-change-rules]
Breaking changes include removing or renaming a method/path, changing status or established semantics, adding a required field, narrowing accepted input, changing a field type/order/bit width, reusing an enum or reserved ID, changing authentication/trust boundaries, or changing Webhook payload, success, or delivery semantics.
Every breaking change must:
1. introduce a new contract or protocol version rather than overwrite old semantics;
2. update schemas, bilingual docs, source-drift tests, and migration guidance;
3. state server and SDK support windows, rollback boundaries, and verification evidence;
4. preserve retired or reserved Frame, Reason Code, and node RPC IDs without reuse.
A new optional field or operation is additive only when old consumers can safely ignore it and the trust boundary is unchanged. Experimental and internal surfaces may evolve, but must stay explicitly labeled and must not silently become public dependencies.
# Webhook Event Types (/en/api/webhooks/events)
The asynchronous notification runtime emits these three events. `focus_events` filters these exact names; an empty list enables all of them.
| Event | Trigger boundary | Body / default batch |
| ------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `msg.notify` | after quorum-durable commit of an ordinary message | message array; up to 100 items or 500ms |
| `msg.offline` | when an eligible durable message has recipients without an online route | one flattened object; up to 512 UIDs per chunk |
| `user.onlinestatus` | when a UID owner's local active session comes online or its last local session goes offline | status-string array; up to 512 items or 2s |
`msg.before_send` is independently enabled synchronous admission; its response allows, replaces, or rejects the send. Queue and retry semantics below apply only to the three asynchronous notifications. See [Before-send business callback](/en/guide/integration/webhooks#before-send-business-callback) for configuration, requests, and failure policies.
## `msg.notify` [#msgnotify]
Use this event for audit, indexing, or asynchronous product notification. It runs after durable commit. A webhook failure neither rolls the message back nor changes a successful SENDACK into a failure.
## `msg.offline` [#msgoffline]
A commit is eligible for offline-candidate calculation only when all of these are true:
* `message_seq > 0`;
* `sync_once` is false;
* the message has no `message_scoped_uids` exact-target list;
* the recipient belongs to the durable delivery set and currently has no online route.
This event is a **push candidate**, not unread-message truth and not a replacement for reconnect synchronization. A recipient may come online, or routing state may change, after the callback was admitted.
## `user.onlinestatus` [#useronlinestatus]
This event reports session transitions observed at the UID owner. It is a compatibility string, not a globally ordered presence log, and intermediate states can be missing. See [Payloads](/en/api/webhooks/payloads) for its format.
Plugin Send/Receive/PersistAfter hooks, cluster RPC, and the benchmark `EVENT` frame are not HTTP webhook events.
# Webhooks (/en/api/webhooks)
Webhooks POST committed messages, offline-candidate recipients, and presence changes to one product endpoint. This is a server-outbound protocol, not a client inbox and not an inbound WuKongIM HTTP API `path`.
[Download the OpenAPI 3.1 Webhook contract](/contracts/webhooks.openapi.json)
`msg.before_send` is independently enabled synchronous admission; its response allows, replaces, or rejects the send. Queue and retry semantics below apply only to the three asynchronous notifications. See [Before-send business callback](/en/guide/integration/webhooks#before-send-business-callback) for configuration, requests, and failure policies.
## Request [#request]
```text
POST {http_addr}?event={event_name}
Content-Type: application/json
```
Existing query parameters are preserved and `event` is set or replaced. The event name exists only in the query. There is **no common body envelope**; each event has its own top-level JSON shape.
| Event | Top-level JSON |
| ------------------- | -------------------------------------------------- |
| `msg.before_send` | one uncommitted message; synchronous send decision |
| `msg.notify` | array of committed messages |
| `msg.offline` | flattened message object with offline UIDs |
| `user.onlinestatus` | array of compatibility status strings |
## Configuration [#configuration]
A non-empty `webhook.http_addr` enables asynchronous notifications. `focus_events=[]` delivers all three events.
```toml
[webhook]
http_addr = "https://events.example.com/wukongim"
focus_events = ["msg.notify", "msg.offline", "user.onlinestatus"]
queue_size = 1024
workers = 16
msg_notify_batch_max_items = 100
msg_notify_batch_max_wait = "500ms"
online_status_batch_max_items = 512
online_status_batch_max_wait = "2s"
offline_uid_batch_size = 512
request_timeout = "5s"
retry_max_attempts = 3
```
The current runtime uses node-memory queues with no disk outbox or crash replay. Receivers should durably enqueue and deduplicate before returning HTTP 200 promptly.
# Webhook Payloads (/en/api/webhooks/payloads)
The event name travels in `?event=...`; request bodies have no common envelope.
## `msg.before_send` [#msgbefore_send]
The synchronous callback receives one uncommitted message and returns an allow, Payload replacement, or rejection decision. See [Before-send business callback](/en/guide/integration/webhooks#before-send-business-callback) for configuration and failure policies, and the OpenAPI reference below for all fields.
## Message object [#message-object]
The `msg.notify` body is an array of these objects. `msg.offline` flattens the same object into its top level.
| Field | JSON type | Meaning |
| ------------------- | ---------------- | ------------------------------------------------------------------------------ |
| `header.no_persist` | number | always `0` for these committed messages |
| `header.red_dot` | number | `0` or `1` |
| `header.sync_once` | number | `0` or `1` |
| `setting` | number | WKProto Setting bit mask |
| `topic` | string, optional | omitted when empty |
| `expire` | number (u32) | expiry value |
| `message_id` | number (u64) | global message ID; JavaScript can lose precision |
| `message_idstr` | string | decimal `message_id`; prefer this across languages |
| `client_msg_no` | string | client idempotency identifier |
| `message_seq` | number (u64) | sequence within the Channel |
| `from_uid` | string | sender UID |
| `channel_id` | string | Channel ID |
| `channel_type` | number (u8) | Channel Type |
| `timestamp` | number (i32) | Unix seconds |
| `payload` | string \| null | non-empty Go `[]byte` uses standard Base64; an empty Payload encodes as `null` |
```json
[
{
"header": {"no_persist": 0, "red_dot": 1, "sync_once": 0},
"setting": 0,
"expire": 0,
"message_id": 123456789,
"message_idstr": "123456789",
"client_msg_no": "order-0001",
"message_seq": 42,
"from_uid": "system",
"channel_id": "u1001",
"channel_type": 1,
"timestamp": 1785398400,
"payload": "eyJ0eXBlIjoib3JkZXJfdXBkYXRlIn0="
}
]
```
## `msg.offline` [#msgoffline]
In addition to the message fields, the top level has one of these **mutually exclusive** recipient representations:
| Condition | Fields |
| ----------------------------------------- | ------------------------------------------------- |
| UID count below the compression threshold | `to_uids: string[]` |
| UID count at or above the threshold | `compress: "gzip"` and `compress_to_uids: string` |
`compress_to_uids` is a JSON string array, gzip-compressed, then standard-Base64 encoded. The default `offline_uid_batch_size=512` is both the UID chunk limit and compression threshold. Numeric `source_id` is included when the sending node ID is nonzero.
## `user.onlinestatus` [#useronlinestatus]
The body is an array of strings in this format:
```text
{uid}-{device_flag}-{online:0|1}-{session_id}-{device_online_count}-{total_online_count}
```
Counts come from the UID owner's local active-session projection. A UID can contain hyphens. Treat the whole record as opaque for compatibility, or parse the final five numeric fields from the right; do not use a fixed naïve `-` split.
# Webhook Reliability & Security (/en/api/webhooks/reliability-and-security)
`msg.before_send` is independently enabled synchronous admission; its response allows, replaces, or rejects the send. Queue and retry semantics below apply only to the three asynchronous notifications. See [Before-send business callback](/en/guide/integration/webhooks#before-send-business-callback) for configuration, requests, and failure policies.
## Delivery semantics [#delivery-semantics]
For asynchronous notifications, only HTTP `200` is success. Every other status, connection error, or timeout fails the attempt. The response body is drained and discarded; it has no protocol meaning.
| Setting | Default | Scope |
| -------------------------------------- | ----------: | ------------------------------------------- |
| `queue_size` | 1024 | in-memory waiting capacity per event class |
| `workers` | 16 | concurrent senders per event class |
| `msg_notify_batch_max_items` / wait | 100 / 500ms | `msg.notify` batch |
| `online_status_batch_max_items` / wait | 512 / 2s | `user.onlinestatus` batch |
| `offline_uid_batch_size` | 512 | offline UID chunk and compression threshold |
| `request_timeout` | 5s | each HTTP attempt |
| `retry_max_attempts` | 3 | total attempts, including the first |
Retries run immediately inside the admitted job, with **no backoff or delay**. A continuously failing request is dropped after three attempts.
Queues are bounded and node-memory only. Queue saturation, process failure, cancellation, timeout, or exhausted retries can lose an event; shutdown drains only until its Context expires. A lost response can produce a duplicate, and workers/batches provide no public global ordering. Webhook failure does not roll back durable commit or a successful SENDACK.
## Receiver contract [#receiver-contract]
Critical product state must remain reconstructable from the product database or message history.
## Security boundary [#security-boundary]
The sender adds only `Content-Type: application/json`. It adds **no signature, shared-secret, or Authorization header**. Do not place long-lived secrets in the webhook URL where logs may capture them.
Production controls must establish trust outside this protocol:
* use HTTPS only; prefer a private network, service mesh, or fixed egress proxy;
* enforce mTLS, fixed egress identity, or controlled credentials at a reverse proxy;
* limit source IPs, rate, and body size;
* avoid logging complete Payloads or personal data;
* never expose the callback as an anonymous public write endpoint.
# Channel (/en/guide/core-concepts/channels)
A Channel is a message “address.” Its complete identity is **`channel_id + channel_type`**. The same ID with a different type identifies a different Channel.
## Why Channels matter [#why-channels-matter]
Choose a Channel before sending. It defines the recipient scope and numbers, orders, and recovers its ordinary persistent messages. Notifications, support, live interaction, and Agents can reuse this entry point.
## Relationship to other concepts [#relationship-to-other-concepts]
A Device does not change Channel identity. Users can have different unread and visibility state in their Conversations.
## Direct chat: send to the peer UID [#direct-chat-send-to-the-peer-uid]
Callers always use the **peer UID**. WuKongIM normalizes both viewpoints internally. Do not construct the internal Channel ID or write it back as a contact ID.
## Group chat: send to a stable group ID [#group-chat-send-to-a-stable-group-id]
| Task | Owner |
| ------------------------------------ | ------------------------------------------------------------------------- |
| Choose the Channel ID | The product supplies a stable group ID |
| Create, invite, or leave | The product decides; a trusted backend synchronizes membership and policy |
| Persist, order, and deliver messages | WuKongIM |
**Knowing the group ID does not grant send permission.** Channel Type is not authorization. The product owns membership, roles, deny/allow lists, and content policy.
Group metadata, specialized types, and large groups
The product owns group names, avatars, announcements, roles, tenant isolation, and moderation. WuKongIM stores synchronized membership and policy and provides message persistence, realtime delivery, and offline recovery.
The product also owns contact, stranger, and send policy for direct chats. Use specialized community, feed, live, visitor, or Agent types only when the server, selected SDK, and product flow support them. Values are listed in [Channel Types](/en/api/dictionaries/channel-types).
For 100,000-member groups, bound membership updates and online delivery work and use paging. See [Large Groups](/en/guide/tutorials/large-groups).
Continue with [User](/en/guide/core-concepts/users) and [Conversation](/en/guide/core-concepts/conversations). For replicas, commit, and migration, see the [Channel Messaging Layer](/en/server/architecture/channels).
# Conversation (/en/guide/core-concepts/conversations)
A Conversation is **one row in one user's chat list**. It points to a Channel and shows the latest message, unread count, and personal state. Messages are stored in the Channel.
## Why conversations matter [#why-conversations-matter]
Conversations help users find chats, spot new messages, hide a chat, or clear unread. During offline recovery, clients first sync the conversation list, then fetch missing messages for each Channel.
## Relationship to other concepts [#relationship-to-other-concepts]
These are example states for two members: **one Channel, different personal read positions and visibility ranges**. Devices sharing a UID sync conversation state while retaining their own local UI state.
## Distinguish Channel and Conversation [#distinguish-channel-and-conversation]
| Channel | Conversation |
| ---------------------------------------------- | --------------------------------------------------------------------- |
| Shared communication space and ordered history | The current user's personal view of a Channel |
| Identified by `channel_id + channel_type` | Also requires the current UID |
| Stores committed messages | Refers to the latest message and presents unread and visibility state |
## Common actions [#common-actions]
| Action | Result |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Clear unread | Advances the current UID's unread baseline; sends no peer-read receipt |
| Hide or delete | Hides your current view; does not leave the group or delete others' messages. A later message above the hidden range can make it appear again |
| Leave or remove a member | Changes Channel membership |
| Sync devices | Other devices with the same UID can see conversation and unread updates on their next sync |
The client owns pinning, drafts, grouping, and final UI ordering.
Integration: list fields, ordering, and sync cursors
A Conversation normally includes Channel ID and type, latest message, unread count, and read and visibility boundaries. These determine which chat opens, which preview appears, and the unread baseline.
The server orders the active conversation directory by `activated_at`: the time of explicit Channel activation, **not latest-message time**. An inactive empty Channel with no visible messages is omitted. The client chooses final ordering after receiving latest-message data.
**The conversation list and each Channel's message list have separate sync positions.** One cursor cannot replace both. WuKongIM builds the view from the user's Channel relationship, committed messages, and personal visibility state.
Continue with [Message](/en/guide/core-concepts/messages), then choose a [full SDK platform](/en/sdk/wukongim) for conversation APIs. For server construction and paging, see [message-flow architecture](/en/server/architecture/message-flow).
# Device (/en/guide/core-concepts/devices)
A Device is a concrete client endpoint through which a user accesses WuKongIM, such as a mobile app, browser, or desktop application. One user can own multiple devices and may use the messaging service from several devices at once.
A Device is not the User and is not one network connection. A network change or reconnect creates another connection, while user and device identity normally remain stable.
## Why devices matter [#why-devices-matter]
* **Enable multi-endpoint use:** One UID can send and receive from phone, Web, and desktop clients.
* **Define synchronization scope:** Ordinary persistent messages and conversations are generally UID-scoped, while drafts, download progress, and push tokens belong to a concrete device.
* **Control online policy:** A product can allow same-category devices to coexist or let a new connection replace an old one.
* **Support removal and revocation:** Removing a device requires more than disconnecting its current socket; credentials and product device state also need cleanup.
## Relationship to other concepts [#relationship-to-other-concepts]
* A **User** can own multiple devices, all sharing one UID.
* An ordinary persistent **Message** leaves through one concrete connection and may reach the user's other active devices or synchronize later.
* A **Channel** and its persistent history do not belong to one device; a replacement device can recover them when authorized.
* A **Conversation** is generally UID-scoped, while each device owns final presentation and local caches.
## Four terms that are easy to confuse [#four-terms-that-are-easy-to-confuse]
| Term | Meaning | Example |
| -------------------- | --------------------------------------- | -------------------------------------- |
| UID | Product user identity | `alice_123` |
| Device Flag | Client category | APP, WEB, PC |
| Device ID | One concrete installation or endpoint | `alice_iphone_01` |
| Connection / Session | One current TCP or WebSocket connection | Changes after disconnect and reconnect |
Device Level describes conflict policy for connections under the same UID and Device Flag, such as allowing several Web endpoints or keeping only one active. It is not a product permission such as administrator, paid member, or group role.
See [Device Flags](/en/api/dictionaries/device-flags) for current values and enforcement boundaries.
## Which state is shared across devices [#which-state-is-shared-across-devices]
| Generally shared by UID | Generally owned by one device |
| -------------------------------------------------------- | ------------------------------------------------------------------- |
| Ordinary persistent Channel messages and message order | Local drafts, scroll position, and download progress |
| Conversations, unread baseline, and hidden range | Provider push token, notification permission, and background limits |
| Account and group relationships from the product backend | Local cache, encryption material, and device risk information |
When one device clears unread state, another device may observe the new value during its next synchronization. An ordinary persistent message may also reach multiple active connections; each device still handles receive, deduplication, and local storage independently.
## A typical device lifecycle [#a-typical-device-lifecycle]
A client cannot promote its own Device Level. The default Gateway adopts the stored Device Level only after the CONNECT token matches that device record, so only a trusted backend may write token-management routes. Keep expiry, replay, and risk policy in the [release checks](/en/guide/integration/acceptance).
Continue with [Conversation](/en/guide/core-concepts/conversations) and [Authentication & Security](/en/guide/integration/authentication).
# Core Concepts (/en/guide/core-concepts)
You do not need to understand nodes, Raft, or data sharding before integrating WuKongIM. Start with five product concepts and most application guides will make sense:
| Concept | Question it answers | In one sentence |
| ------------ | --------------------------------- | -------------------------------------------------------------------------------- |
| Message | What is being sent? | A piece of content such as text, an image, a notification, or a business command |
| Channel | Where is it sent? | The message target and recipient scope, such as a direct chat or group chat |
| User | Who is communicating? | A product-defined communication identity with a stable UID |
| Device | From which endpoint? | A phone, browser, desktop client, or another client instance used by a user |
| Conversation | How does a user return to a chat? | That user's view of a channel, usually with a latest message and unread count |
## How the five concepts work together [#how-the-five-concepts-work-together]
Consider Alice sending a message to Bob:
For a group chat, the channel changes from a direct target to a group ID and its members. The basic relationship among users, devices, messages, and conversations stays the same.
## WuKongIM and your product system [#wukongim-and-your-product-system]
WuKongIM is communication infrastructure. It does not replace your account and product services.
| Your product system owns | WuKongIM owns |
| ------------------------------------------------------------ | ----------------------------------------------------------- |
| Registration, sign-in, profiles, contacts, and group details | Client connections, message routing, and realtime delivery |
| Group membership, roles, content policy, and moderation | Per-channel order, persistence, and offline synchronization |
| Product-token issuance, risk controls, and mobile push | Online device connections and UID-scoped conversation data |
Core Concepts explains product semantics. For nodes, Slots, Leaders, replicas, and failover, go to [Server Architecture](/en/server/architecture).
## Explore each concept [#explore-each-concept]
With these concepts in place, run the [Quick Start](/en/guide/quick-start) and exchange your first message.
# Message (/en/guide/core-concepts/messages)
A Message is the basic unit of information transported by WuKongIM. Text, images, files, notifications, audio/video signaling, and IoT commands can all be represented as message content.
An ordinary persistent message enters its target Channel for realtime delivery and later offline synchronization. Your application defines what the content means and how clients display or process it; specialized transient and command branches have different boundaries.
## Why messages matter [#why-messages-matter]
* **Carry product content:** The same messaging path can carry chat content or custom business events.
* **Work online and offline:** An ordinary persistent message can arrive in realtime and be recovered from committed Channel history after reconnecting.
* **Support tracking and deduplication:** A client send number, server message ID, and per-Channel sequence serve different retry, lookup, and ordering needs.
## Relationship to other concepts [#relationship-to-other-concepts]
* A **Channel** determines the target, possible recipients, and ordering boundary.
* A **User** sends a message or participates in its Channel.
* A **Device** is a concrete endpoint for sending, realtime receive, and later recovery.
* A **Conversation** presents the Channel's latest message and unread projection to one user.
## What a message contains [#what-a-message-contains]
For an application developer, a message answers “who sent what, and where?”
| Information | Purpose | Common field |
| ---------------------------- | ----------------------------------------------------- | ---------------------------- |
| Sender | Identifies the user who sent the message | `from_uid` |
| Target | Identifies the Channel that owns the message | `channel_id`, `channel_type` |
| Content | Holds text, image metadata, or custom data | `payload` |
| Client send number | Identifies one logical send for idempotent retry | `client_msg_no` |
| Server identity and position | Supports lookup, deduplication, and per-Channel order | message ID, `message_seq` |
For example, an application might define a text Payload as:
```json
{
"type": 1,
"content": "Meet at 9 a.m. on Saturday"
}
```
Your business and clients agree on the meaning of `type` and the remaining fields. Version custom Payloads and define backward-compatible behavior.
## The life of an ordinary persistent message [#the-life-of-an-ordinary-persistent-message]
A send acknowledgement says that the send reached its defined server boundary. It does not prove that every device received the message, and it does not prove that a person read it. Model delivery receipts, read receipts, and business execution results separately.
## Three identifiers that are easy to confuse [#three-identifiers-that-are-easy-to-confuse]
| Identifier | Created by | Typical use |
| --------------- | ------------------- | --------------------------------------------------------------------------- |
| `client_msg_no` | Sender | Idempotently retry the same logical send; keep it unchanged |
| Message ID | Server | Identify one server-accepted message for result correlation and diagnostics |
| `message_seq` | Channel message log | Represent the message's position and order inside that Channel |
Message order is meaningful only within one Channel. Never compare `message_seq` values from different Channels to infer a global order.
## Remember when building [#remember-when-building]
* Use ordinary persistent messages for anything that must be recoverable or reliably delivered.
* Reuse the original `client_msg_no` when retrying the same send; do not create another business message.
* Treat Payload as a product contract: bound its size, validate its fields, and version it.
* Do not infer behavior from names such as `NoPersist` or `SyncOnce`; read [Message Flags](/en/api/dictionaries/message-flags) before using them.
Continue with [Channel](/en/guide/core-concepts/channels). For integration, read [Messaging](/en/guide/integration/messaging); for commit, replication, and delivery internals, read [Message Flow](/en/server/architecture/message-flow).
# User (/en/guide/core-concepts/users)
A User is the identity that sends and receives messages in WuKongIM, uniquely identified by a UID. It may represent a person, support account, bot, Agent, or another trusted service identity.
The UID comes from your product system. WuKongIM uses it for connection, routing, device association, and message delivery, but it does not replace registration, sign-in, user profiles, or product authorization.
## Why users matter [#why-users-matter]
* **One communication identity:** The same UID still represents the same product user on phone, Web, and desktop clients.
* **Connect the other concepts:** Messages have senders, devices belong to users, and conversations are built from one user's viewpoint.
* **Anchor product boundaries:** Account bans, tenant isolation, contacts, and group roles all depend on a trusted user identity.
## Relationship to other concepts [#relationship-to-other-concepts]
* A **Message** records a sender UID and reaches other users through a Channel.
* A **Channel** organizes communication among users; a direct-chat target is normally the peer UID.
* A **Device** belongs to one UID, and one user can have multiple devices.
* A **Conversation** belongs to a user's viewpoint, so unread and visibility state can differ across users in the same Channel.
## Designing a UID [#designing-a-uid]
A good UID is:
* stable for the lifetime of a product account and never reassigned to another user;
* selected by a trusted backend, never freely chosen by a client;
* independent of display names, phone numbers, and mutable email addresses;
* handled in logs and metrics without exposing unnecessary personal information.
If Alice changes her display name or phone, for example, her UID can remain `alice_123`. Message history, Channel membership, and conversations remain attached to the same identity.
## What WuKongIM knows [#what-wukongim-knows]
| WuKongIM needs | The product system still stores and decides |
| ------------------------------------------------------------------ | ------------------------------------------------------------- |
| Stable UID and connection device information | Passwords, OAuth identity, phone number, and profile |
| Messages, Channels, and conversation state associated with the UID | Contacts, group roles, subscriptions, and content permissions |
| A transient view of active connections | Reliable login history, risk decisions, and account status |
Online state means “an active connection is observed now.” It is not a sufficient source of truth for payments, authorization, or one-time business actions.
## How a user connects [#how-a-user-connects]
1. The client signs in to your product service.
2. The product backend verifies the account and permissions and maps it to a stable UID.
3. A trusted backend issues connection material for the current device.
4. The client connects to WuKongIM with the UID, device information, and token.
5. After the connection succeeds, the client can send messages and synchronize conversations as that UID.
The current v3 Beta default composition exactly matches each CONNECT token against the stored token for the same UID and device category, but this is not a complete account or authorization system. Clients must not call WuKongIM HTTP API directly; a trusted backend must own issuance, expiry, rotation, and revocation and protect token-management routes.
Continue with [Device](/en/guide/core-concepts/devices) and [Identity & Token](/en/guide/integration/authentication). For distributed online routing, read [User Connection Routing](/en/server/architecture/user-routing).
# Release checks (/en/guide/integration/acceptance)
Goal: prove the production messaging loop and recovery from critical failures with real clients, server configuration, and application APIs.
## 1. Freeze the versions [#1-freeze-the-versions]
Record exact SDK versions and lockfiles, the server version and configuration, and client/device/network coverage. Include login, token, history, conversation, and media APIs plus data changes and rollback. Release dependencies, configuration, and lockfiles as one change unit.
## 2. Run the six-step messaging loop [#2-run-the-six-step-messaging-loop]
Use Alice and Bob in independent clients. Retain screenshots or redacted results, with expected and actual outcomes for each step.
Sending, live reception, and offline synchronization are **different events**. Verify each separately.
## 3. Verify the SDK's recovery path [#3-verify-the-sdks-recovery-path]
| Integration | Step 5 acceptance |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Full SDK | Disconnect Bob, send another message from Alice, then reconnect and restore persistent messages; verify order, deduplication, conversations, and unread |
| EasySDK online path | Verify disconnect/reconnect/error callbacks, listener removal, and instance destruction; do not assume automatic offline sync |
| EasySDK with product history | Also verify your backend's history fetch, deduplication, conversations, and unread; promise these only after implementing them |
Ordinary and command-style NoPersist have no persistent history and cannot prove offline recovery.
## 4. Meet the release criteria [#4-meet-the-release-criteria]
| Required check | Passing condition |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Identity and network | Invalid, revoked, and wrong-UID tokens rejected; product expiry enforced; TLS/WSS and no client management credentials |
| Messaging and connection | Correct results and UI for sending, failure, timeout, retry, duplicates, network changes, and restart |
| Capacity and diagnosis | Target online users, message rate, and group size reached; queues/retries/degradation bounded; metrics and redacted logs support diagnosis |
| Data and rollback | Account isolation, upgrades, and recovery rehearsed; rollback uses compatible data or a complete previous-generation backup |
Additional checks for product features
| Feature in use | Verify |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Multiple devices and conversations | Latest message, unread, deletion, and multi-device convergence; clear UI for foreground/background, kick-out, and token invalidation |
| Media | Upload failure, cancellation, timeout, URL permissions, and file cleanup |
| Local persistence | Backup, encryption, logout cleanup, and upgrade migration |
| Operations APIs | WuKongIM HTTP API, Manager, and diagnostics behind protected management boundaries, without direct public exposure |
Your backend rotates or revokes expired tokens. CONNECT does not automatically inspect expiry inside the token string. Test the mechanism you actually deploy, beyond a client-side timer.
Incomplete identity or network boundaries, tokens/full messages in logs, unrecoverable critical failures, missed capacity targets or absent rate-limit/degradation policy, or irreversible data changes without a reviewed forward-repair plan.
Start with a small rollout. Observe connection success, send failures, latency, sync errors, queue levels, and storage errors before expanding traffic.
Next: [Health & Monitoring](/en/server/operations/health-and-monitoring) and [Upgrade & Migration](/en/server/operations/upgrade-and-migration). See [Authentication & Security](/en/api/authentication) for security boundaries.
# Integration Architecture (/en/guide/integration/architecture)
A reliable integration separates the product control plane from the messaging data plane. The product service decides who may do what; the WuKongIM cluster moves and stores messages in real time and in channel order.
## Responsibility split [#responsibility-split]
| Component | Owns | Does not own |
| ---------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------- |
| End-user app | User experience, SDK lifecycle, local state, message display | Trusted identity issuance or server authorization |
| Client SDK | Routing, connection, protocol frames, reconnect, send, receive, acknowledgements | Product accounts, group policy, or moderation |
| Product service | Login, UIDs, tokens, relationships, membership, content policy, webhook side effects | Long-lived connections or channel-log replication |
| WuKongIM cluster | Gateway, channel ordering, persistence, routing, online delivery, cluster failover | Product databases or final business rules |
Every deployment is a cluster. A one-node deployment is a single-node cluster; it does not bypass Controller, Slot, Channel, or routing semantics.
## Session bootstrap [#session-bootstrap]
Connection state is not product authorization state.
The current `/route` endpoint returns configured ingress addresses and does not validate the UID's product permissions. The ability to discover a route is not proof of authorization.
## Two message entry paths [#two-message-entry-paths]
* **Client path:** an SDK sends over a long-lived connection and receives SENDACK, RECV, and connection-state events.
* **Server path:** a trusted product service calls `/message/send` for system-originated or product-triggered messages.
Both paths converge on the same message use case and channel append path. A product service must not write node-local storage directly or bypass cluster routing because a deployment currently has one node.
## Failure boundaries [#failure-boundaries]
* For a default persistent message, a successful SENDACK means the channel authority made a successful durable-append decision.
* Ordinary and command-style `NoPersist` enter transient online delivery with a message ID and sequence zero. Neither supports durable history or offline recovery; see [Message Flags](/en/api/dictionaries/message-flags).
* Online delivery, webhooks, and plugins are independent post-commit effects; conversation construction happens later during membership sync.
* A webhook failure does not turn an already-successful SENDACK into a failure.
* Client disconnect recovery uses SDK reconnect and message synchronization, not webhook replay.
For the internal commit and delivery boundaries, read [Server Architecture](/en/server/architecture) and [Message Flow](/en/server/architecture/message-flow). Continue with [Authentication](/en/guide/integration/authentication) to tighten the trust boundary before implementing messaging.
# Authentication (/en/guide/integration/authentication)
Goal: after product login, the SDK connects to WuKongIM using connection material from your backend.
## 1. Sign in to your product [#1-sign-in-to-your-product]
Your backend validates the account, tenant, device, and risk policy. Use a stable, non-recycled UID, never a display name.
## 2. Prepare credentials on the backend [#2-prepare-credentials-on-the-backend]
Generate a high-entropy token, store it through protected WuKongIM HTTP API, and discover the route. Return only the current user's `uid`, device parameters, token, and WebSocket address.
**Only trusted backends call token-management and routing APIs.** WuKongIM HTTP API has no general product-authentication middleware. CORS permits browser access; it does not grant authorization.
Store a token: request and response
Call [`POST /user/token`](/en/api/product-http/users/setQuickstartUserToken) from your backend:
```json
{
"uid": "u1001",
"token": "replace-with-a-random-secret",
"device_flag": 0,
"device_level": 1
}
```
A successful `{"status": 200}` means the token was stored, not that old connections have closed.
## 3. Connect the SDK to Gateway [#3-connect-the-sdk-to-gateway]
CONNECT uses the same UID, `device_flag`, and token. With the default `gateway.token_auth_on=true`, empty tokens, missing device records, or mismatches are rejected with `ReasonAuthFail`.
Use HTTPS/WSS in production. Keep tokens out of URLs, logs, analytics, and unprotected client storage.
**Your backend owns token expiry.** The current API has no expiry field and CONNECT checks the stored token. Rotate or revoke it at expiry; a client-side timer is insufficient.
Device fields and connection boundaries
| Field | Usage |
| -------------- | --------------------------------------------------------------------------------------- |
| `device_flag` | Platform class: APP `0`, WEB `1`, PC `2`; backend storage and SDK connection must agree |
| `device_id` | Concrete installation or endpoint; the product defines reinstall behavior |
| `device_level` | The backend owns master/slave policy; clients cannot self-promote |
WKProto starts with CONNECT. Gateway retains identity and the negotiated protocol version, then activates online routing after success. Default session encryption does not replace product authentication or TLS. Use `gateway.token_auth_on=false` only for controlled compatibility migration.
Keep WuKongIM HTTP API on a private network or service mesh behind service-identity verification. Authorize token, send, and membership operations separately. Rate-limit credential routes, use TLS, and audit callers, request IDs, and outcomes without plaintext tokens.
Logout, revocation, and rotation
Destroy the client connection on logout. Your backend calls [`POST /user/device_quit`](/en/api/product-http/users/quitUserDevice):
```json
{
"uid": "u1001",
"device_flag": 0
}
```
This clears the stored token for the selected device class, rejects later CONNECT attempts, and schedules matching connections on the handling node for closure. Verify closure and reconnect rejection in production and synchronize product revocation, rotation, and audit state.
Tokens are exact-match credentials. A JWT `exp` inside the string does not expire it automatically. Test invalid, revoked, wrong-UID tokens and your backend's expiry policy before release.
Next: [Messaging](/en/guide/integration/messaging).
# Integration (/en/guide/integration)
WuKongIM owns real-time connections, channel messaging, persistence, cluster routing, and online delivery. Your product service still owns accounts, authorization, business relationships, content policy, and final business actions.
The current v3 Beta app performs CONNECT negotiation, Session encryption, and exact stored device-token validation by default. WuKongIM HTTP API routes still have no product-authentication middleware, and the built-in token record has no expiry timestamp or replay policy. Put these routes behind a trusted network or authenticated reverse proxy, and let the product identity system own credential issuance, rotation, and revocation.
## Recommended path [#recommended-path]
Before choosing platform APIs, learn the shared vocabulary in [WuKongIMSDK Core Concepts](/en/sdk/wukongim/concepts). Then choose your platform under [WuKongIMSDK](/en/sdk/wukongim) and follow its task sequence.
## Minimum product loop [#minimum-product-loop]
1. The user signs in to your product service instead of proving product identity directly to WuKongIM.
2. The product service assigns a stable UID, device identity, and short-lived credential.
3. The client discovers a route and connects to the Gateway through an SDK or protocol implementation.
4. A client or trusted product service sends messages and checks the protocol-level result.
5. The product service receives asynchronous webhooks and places side effects on its own durable queue.
6. After a disconnect, the client SDK reconnects and recovers messages; webhooks are not a client inbox.
## Pre-production checklist [#pre-production-checklist]
| Area | What you must confirm |
| ------------- | ----------------------------------------------------------------------------------------- |
| Identity | UIDs have one authority and untrusted clients cannot mint tokens |
| Network | WuKongIM HTTP API routes are not exposed directly and Gateway ingress is controlled |
| Authorization | Relationships, membership, allowlists, denylists, and content rules follow product policy |
| Messages | Payloads are versioned and senders retain `client_msg_no` |
| Callbacks | The receiver persists first, returns 200 second, and tolerates duplicates |
| Operations | Queue pressure, failures, retry exhaustion, and end-to-end latency are monitored |
Begin with stable server boundaries and webhooks. Introduce [Plugin Extensions](/en/guide/integration/plugins) only when a synchronous or post-commit hook genuinely needs to run near a node.
After implementation, use the [Release Checks](/en/guide/integration/acceptance) with your real clients, server configuration, networks, and rollback plan.
# Messaging (/en/guide/integration/messaging)
Goal: Alice sends, checks the server result, and verifies reception in Bob's independent client.
## 1. Confirm the target [#1-confirm-the-target]
Both clients must be connected. A direct chat targets the **peer UID**. Groups use a stable group ID and the correct Channel Type; your backend owns membership and send permissions.
## 2. Check sending and reception separately [#2-check-sending-and-reception-separately]
| Observation | What it proves |
| --------------------------------------------------- | -------------------------------------------------- |
| Successful SENDACK for a default persistent message | The server committed it durably |
| Bob receives the message | Online delivery to Bob |
| Bob read or processed it | Requires your product's receipt or business result |
**SENDACK is not a read receipt; HTTP 200 is not protocol success.** Check SENDACK or HTTP `reason`. With EasySDK, handle the selected version's send result as shown in its [platform tutorial](/en/sdk/easy).
① Bob receives Alice's message. ② Bob replies. This capture verifies bidirectional exchange, without proving a peer-read receipt.
## 3. Handle failure and recovery [#3-handle-failure-and-recovery]
| Outcome | Action |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Explicit rejection | Use [Reason Codes](/en/api/dictionaries/reason-codes) to correct identity, permissions, or inputs |
| Timeout or unknown outcome | Retain the original identity, check the outcome, and use the SDK's supported retry method |
| Network loss or reconnect | Full SDKs sync missing persistent messages; EasySDK history and conversation recovery require product implementation |
`message_id` is the server-assigned identity. `message_seq` orders messages **within one Channel**, not globally. The sender generates and retains `client_msg_no` for correlation and retries.
Backend send: HTTP example
Only a trusted backend calls [`POST /message/send`](/en/api/product-http/message-send/sendChannelMessage). Do not expose it directly to browsers or mobile clients. WuKongIM HTTP API has no general product authentication; use a private network or protected proxy. Encode `payload` as Base64.
```bash
curl -sS http://127.0.0.1:5001/message/send \
-H 'Content-Type: application/json' \
-d '{
"from_uid": "system",
"channel_id": "u1001",
"channel_type": 1,
"client_msg_no": "order-20260730-0001",
"payload": "eyJ0eXBlIjoib3JkZXJfdXBkYXRlIiwidGV4dCI6IlNoaXBwZWQifQ=="
}'
```
Example accepted-commit response:
```json
{
"message_id": 123456789,
"message_seq": 42,
"reason": 1
}
```
Inspect `reason` even with HTTP 200 and distinguish success, retries, rejection, and route changes.
Payload: versions and safe fallback
WuKongIM transports bytes; your product owns the message schema. Use a versioned envelope:
```json
{
"version": 1,
"type": "order_update",
"body": {
"order_id": "o-10001",
"status": "shipped"
}
}
```
Ignore unknown fields and safely degrade on unknown `type` values. Exclude server secrets, access tokens, and unnecessary personal data.
Persistence, side effects, and NoPersist
Client connections and server HTTP share the message use case and Channel append path. After a durable commit, online delivery, conversation-related work, webhook admission/delivery, and plugin hooks can run independently. Failure does not undo the message or extend an already-completed acknowledgement.
Your backend orchestrates stronger consistency with its own database and idempotency keys. Webhooks and SENDACK are not one transaction.
Ordinary and command-style `NoPersist` use transient online delivery: a message ID, sequence zero, and no durable history or offline recovery. See [Message Flags](/en/api/dictionaries/message-flags).
Full-SDK recovery checks
Retain Channel sync progress. After disconnection, rediscover the route, CONNECT, sync missing persistent messages, merge/deduplicate by identity and Channel sequence, and handle RECVACK correctly. Conversation-directory and Channel-message cursors are separate. See [full SDKs](/en/sdk/wukongim) for connection state, backoff, and sync APIs.
EasySDK does not automatically supply this history, conversation, and unread recovery. Implement it through your own backend when required.
Configure [Webhooks](/en/guide/integration/webhooks), then complete [release checks](/en/guide/integration/acceptance).
# Plugin Extensions (/en/guide/integration/plugins)
WuKongIM v3 Beta can extend message paths and host RPC through local `.wkp` processes. Plugins fit controlled extensions that need to run near a WuKongIM node, but they do not replace a business service, durable queue, or cluster-wide deployment orchestrator.
Plugin processes, configuration, and desired/observed lifecycle state belong to each node. Enabling a plugin on one node does not prove that every node has installed, configured, or started the same version.
## When to use a plugin [#when-to-use-a-plugin]
| Need | Recommended boundary |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Synchronously inspect or mutate a payload before SENDACK | Send hook; keep the chain short and accept its latency/availability impact |
| Observe committed messages for selected offline recipients | Receive hook; post-commit, UID-bound, and best-effort |
| Observe every durable commit | PersistAfter; post-commit and best-effort |
| Deliver ordinary asynchronous events to a business system | Prefer a webhook or business-owned queue boundary |
| Complex orchestration, external transaction, or long-running job | Use an independent business service, not a synchronous Send hook |
## Lifecycle [#lifecycle]
When Manager updates configuration, an existing secret represented by `SecretHidden` is preserved, and returned details redact secrets. A running plugin that advertises `ConfigUpdate` receives a synchronous configuration update call. Uninstall first writes disabled desired state, then stops and removes the local process.
## Three hook classes [#three-hook-classes]
| Hook | Timing | Failure semantics |
| ------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Send | After permission succeeds, before Channel append admission | Synchronous; fail-closed by default with explicit fail-open option; may mutate payload or reject |
| Receive | After durable commit for an offline UID batch | Independent recipient failure does not change SENDACK, online delivery, or membership state |
| PersistAfter | After durable commit | Failure reaches worker logs/metrics only and never rolls back the message |
Candidate plugins are ordered by priority descending and plugin number ascending. A Send response may replace only the payload, not sender, Channel, Session, or routing fields. A plugin-origin send still enters the message usecase, permission checks, recursion guard, and Channel authority.
## UID bindings [#uid-bindings]
A UID-to-plugin-number binding is Slot-authoritative metadata routed by the UID physical hash-slot key. It expresses only the cluster relationship; it stores no node's plugin process, configuration, or running state. During Receive, the binding is intersected with running/enabled/method candidates on the current node, and the highest-priority plugin is selected.
Therefore, “a binding exists” does not mean “every node can execute it.” Before rollout, verify compatible plugins on every node that may execute work for the target UIDs.
## Host RPC [#host-rpc]
Current compatibility capabilities include:
* send through the ordinary message usecase;
* read committed messages from an explicit Channel;
* read cluster nodes and physical hash-slot snapshots and resolve a Channel owner;
* read a UID's conversation Channels;
* forward a bounded HTTP request to a plugin route locally or on one specified node.
Host RPC still enforces body, header, query, timeout, and target validation. `toNodeId=-1` fanout is explicitly deferred and does not perform a silent partial broadcast.
## Business HTTP routes [#business-http-routes]
A business backend can call `ANY /plugins/:plugin_no/*path` on the WuKongIM HTTP API listener, for example `POST /plugins/wk.plugin.search/search`. The server forwards the method, path, body, and first value of each header/query entry to the connected plugin's `Route` method on that node, then returns its status, body, and end-to-end headers. The plugin defines its payload schema; these extension routes are separate from the core WuKongIM HTTP API JSON operation inventory.
The entry inherits the WuKongIM HTTP API network boundary and maintenance gate without adding authentication. Request and response bodies are limited to 10 MiB. Execution uses `plugin.timeout` (default 5 seconds). Oversized requests return 413, unavailable plugins or invalid responses return 502, and an unwired plugin runtime returns 503. Hop-by-hop response headers and plugin-supplied `Content-Length` are removed.
This compatibility entry is a fix after `v3.0.0-beta.13`, which returns 404 for this path. Existing integrations require a server containing the fix and acceptance tests against their actual plugin.
## Safety and capacity checklist [#safety-and-capacity-checklist]
1. Install only reviewed plugin artifacts and pin version and digest.
2. Verify installation, desired config, observed methods, and running state per node.
3. Use short external-call timeouts; never run a long task in a synchronous Send hook.
4. Monitor low-cardinality invocation metrics such as `method="send"`, worker queues, failures, and timeouts.
5. Preserve recursion guards for hook re-entry and plugin-origin sends; do not create an infinite message loop.
6. Treat Receive/PersistAfter as fallible effects. When processing must be reliable, enqueue into a business-owned durable queue.
Continue with [Integration Architecture](/en/guide/integration/architecture), [Messaging](/en/guide/integration/messaging), and [Webhooks](/en/guide/integration/webhooks).
# Webhooks (/en/guide/integration/webhooks)
Goal: make a decision before sending, or receive business notifications after a message commits.
## 1. Choose the callback [#1-choose-the-callback]
| Callback | Purpose and result |
| ---------------------------------- | ----------------------------------------------------------------------------------------- |
| Before-send `msg.before_send` | Allow, replace, or reject before submission; return an explicit JSON decision |
| Async notification | Consume committed messages or status events; HTTP `200` acknowledges the callback request |
For a default persistent message, the order is:
**Before-send allowance does not prove commit.** Explicit denial prevents submission; asynchronous failure does not undo a committed message.
## 2. Connect your business service [#2-connect-your-business-service]
For synchronous decisions, start the [Go example](https://github.com/WuKongIM/WuKongIM/tree/main/docs-site/examples/go-webhook) from the repository root. It needs Go 1.22+ and no SDK or database:
```bash
cd docs-site/examples/go-webhook
go run .
```
For asynchronous notifications, implement a receiver that validates the source, parses `event`, and durably deduplicates accepted events before returning HTTP `200`.
**Callbacks have no built-in signature or shared-secret header.** Establish trust through HTTPS and a private network, service mesh, or protected proxy. Client Token authentication does not authenticate callback requests.
The Go example listens only on same-host loopback. In another host or container, `127.0.0.1` refers to that environment; use a reachable trusted endpoint.
Before-send configuration, payload, and failure policy
`msg.before_send` runs synchronously after permission checks and Send plugins, before message submission. It defaults to disabled and is independent of asynchronous Webhook `http_addr`, `focus_events`, and plugin enablement. Configure every sending ingress node consistently; changes require a restart.
```toml
[webhook.before_send]
enabled = true
http_addr = "https://business.example.com/im/webhook"
timeout = "500ms"
on_timeout = "deny"
on_error = "deny"
max_in_flight = 256
```
Environment overrides are `WK_WEBHOOK_BEFORE_SEND_ENABLED`, `WK_WEBHOOK_BEFORE_SEND_HTTP_ADDR`, `WK_WEBHOOK_BEFORE_SEND_TIMEOUT`, `WK_WEBHOOK_BEFORE_SEND_ON_TIMEOUT`, `WK_WEBHOOK_BEFORE_SEND_ON_ERROR`, and `WK_WEBHOOK_BEFORE_SEND_MAX_IN_FLIGHT`. The URL must use HTTP(S), without userinfo or a fragment.
```http
POST /im/webhook?event=msg.before_send
Content-Type: application/json
```
```json
{
"from_uid": "u1",
"channel_id": "group1",
"channel_type": 2,
"client_msg_no": "business-message-001",
"payload": "aGVsbG8=",
"no_persist": false,
"sync_once": false
}
```
`channel_id` identifies the canonical source channel: person channels use the server-normalized identity, SyncOnce uses the source rather than internal command channel, and request-scoped `subscribers` use the stable temporary source channel derived from that recipient snapshot. No committed `message_id` or `message_seq` exists yet. Payloads use Base64. Each group message invokes one callback per ingress send attempt, independent of member count; authority forwarding does not repeat it.
Return HTTP 200 with one explicit JSON decision:
| Outcome | Response |
| ----------------------- | ----------------------------------- |
| Allow unchanged | `{"allow":true}` |
| Allow replacement | `{"allow":true,"payload":"bmV3"}` |
| Standard rejection | `{"allow":false}` |
| Business rejection code | `{"allow":false,"reason_code":200}` |
`allow` is required. Only Payload can change; identity and routing fields remain immutable. Omit `payload` to preserve the input. A replacement must be valid Base64 decoding to 1–32767 bytes. The response is limited to 64 KiB and one JSON object; unknown or duplicate fields, missing decisions, trailing JSON, and invalid field types in allow responses are callback errors. Business codes 128–255 retain their values in SENDACK `reason_code` and WuKongIM HTTP API `reason`, independently of HTTP status. A decoded denial with an omitted or invalid rejection code returns standard NotAllowSend (11); an inapplicable Payload on a denial also remains a denial.
| Condition | Behavior |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Explicit business denial | Always reject, without message submission or delivery |
| Callback timeout | `on_timeout`: `allow` keeps the pre-callback Payload; `deny` returns SystemError (15) |
| Network failure, non-200 status, or no parseable decision | `on_error`: `allow` keeps the input; `deny` returns SystemError (15) |
| `max_in_flight` reached | Immediately return SystemError (15), without another waiting queue |
| Original send canceled or expired | Stop; failure-open policy cannot override it |
Both failure policies default to `deny`. Calls are not automatically retried, redirects are not followed, and callbacks never enter the asynchronous notification queue. The callback is capped by the original send's remaining deadline; leave time for submission. Explicit denial takes precedence over callback failure-open, while parent cancellation always wins.
Client retries may repeat callbacks. Deduplicate by sender, source channel, and `client_msg_no`; callers needing retry correlation must provide a stable nonempty `client_msg_no`. Business approval does not guarantee later message commit, so do not perform irreversible billing solely on this callback. NoPersist and SyncOnce also pass through it while retaining their existing delivery and storage semantics.
Monitor `wukongim_webhook_before_send_total{result}` and `wukongim_webhook_before_send_duration_seconds{result}` for `allow`, `reject`, `timeout_allow`, `timeout_deny`, `error_allow`, `error_deny`, `overloaded`, `invalid_request`, and `canceled`. Labels contain no UID, channel, endpoint, or message content. The client sets only Content-Type, with no built-in signature or authentication header; establish callback trust through a controlled network or proxy.
Asynchronous notification configuration
Configure `wukongim.toml`:
```toml
[webhook]
http_addr = "https://events.example.com/wukongim"
focus_events = ["msg.notify", "msg.offline", "user.onlinestatus"]
queue_size = 1024
workers = 16
request_timeout = "5s"
retry_max_attempts = 3
```
The server puts the event name in the query string:
```text
POST https://events.example.com/wukongim?event=msg.notify
Content-Type: application/json
```
Only HTTP 200 is success. Connection errors, timeouts, and other status codes receive a finite number of attempts within the current in-memory job.
## 3. Verify the result [#3-verify-the-result]
Restart the single-node cluster or all ingress nodes after changing callback configuration; keep production settings consistent across nodes.
Send these three texts through the Go before-send example:
| Text | Expected result |
| ----------------- | --------------------------------------------------------- |
| `hello` | Allow unchanged; reception matches history |
| `[replace] hello` | Receive `Reviewed: hello`; history stores the replacement |
| `[reject] hello` | Send `reason=200`; no history row for the denied message |
For async notifications, commit a test message and verify receipt plus HTTP `200`; confirm business processing separately. Consume duplicates idempotently and test bounded retries and event-loss behavior when the receiver fails.
**Inspect the send result as well as reception and history.** The example's rejection code `200` is a send reason, not an HTTP status. Before-send timeout and error policies default to `deny`; retries may repeat callbacks, so retain the same sender/source/`client_msg_no` identity.
Queue saturation, process exit, request cancellation, or retry exhaustion can lose an event. The current runtime has no disk-backed webhook outbox or crash replay. A webhook failure does not change an already-successful SENDACK or durable append.
Go example requests, replacement rules, and customization
The default endpoint is `http://127.0.0.1:8090/webhook`. Change the port with `go run . -addr 127.0.0.1:8091`; stop with Ctrl+C. First try the callback from another terminal:
```bash
curl -fsS 'http://127.0.0.1:8090/webhook?event=msg.before_send' \
-H 'Content-Type: application/json' \
-d '{"from_uid":"alice","channel_id":"example-group","channel_type":2,"client_msg_no":"example-allow","payload":"aGVsbG8="}'
```
Expect `{"allow":true}`. Replace the Base64 `payload` to exercise each rule:
| Text | Base64 Payload | Result |
| ----------------- | ---------------------- | ------------------------------------------------- |
| `hello` | `aGVsbG8=` | Allow unchanged |
| `[replace] hello` | `W3JlcGxhY2VdIGhlbGxv` | Rewrite to `Reviewed: hello` |
| `[reject] hello` | `W3JlamVjdF0gaGVsbG8=` | HTTP 200 with `allow=false` and `reason_code=200` |
Then configure the callback in your existing `wukongim.toml`:
```toml
[webhook.before_send]
enabled = true
http_addr = "http://127.0.0.1:8090/webhook"
timeout = "500ms"
on_timeout = "deny"
on_error = "deny"
max_in_flight = 64
```
Restart the single-node cluster or every ingress node on the same host, then send the table's text through your existing client. For SDK text Payloads such as `{"type":1,"content":"hello"}`, rules apply to `content` while preserving other JSON fields. Custom message types and binary Payloads pass through unchanged. The example README includes WuKongIM HTTP API group creation, send, and history commands. Successful sends return `reason=1`; this example's business denial returns 200. Received messages and history must contain the rewritten content, with no history row for a denied message.
Customize `evaluate` in `main.go`. Its current rules have no side effects or cache: the same input and rules produce the same decision. Add durable idempotency if your business logic writes data, and do not treat callback allowance as proof of message commit. Logs contain only the decision type. The body is limited to 64 KiB, Payload to 32767 bytes, and each example process to 64 concurrent requests; excess requests receive HTTP 503. A rewrite exceeding the Payload limit explicitly rejects.
The example accepts only loopback bind addresses for processes on the same host. `127.0.0.1` inside another host or container refers to that host or container; remote use needs a reachable trusted proxy and source authentication. Client Token authentication and business callback authentication are separate boundaries.
Event payloads and online-status parsing
| Event | Request body | Typical use |
| ------------------- | ----------------------------------------------- | ------------------------------------------------- |
| `msg.before_send` | One uncommitted message object | Synchronous allow, replace, or reject |
| `msg.notify` | Array of committed messages | Audit, search indexing, asynchronous notification |
| `msg.offline` | One message and a bounded batch of offline UIDs | Offline-push candidate calculation |
| `user.onlinestatus` | Array of compatibility status strings | UID-owner-local, best-effort session hint |
Representative `msg.notify` request:
```json
[
{
"header": {"no_persist": 0, "red_dot": 1, "sync_once": 0},
"setting": 0,
"expire": 0,
"message_id": 123456789,
"message_idstr": "123456789",
"client_msg_no": "order-20260730-0001",
"message_seq": 42,
"from_uid": "system",
"channel_id": "u1001",
"channel_type": 1,
"timestamp": 1785398400,
"payload": "eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoib3JkZXJfdXBkYXRlIn0="
}
]
```
JSON encodes `payload` as Base64. For small recipient sets, `msg.offline` uses `to_uids`. At the compression threshold it may instead use `compress: "gzip"` with Base64-encoded `compress_to_uids`; receivers must support both forms.
Each `user.onlinestatus` item has this form:
```text
{uid}-{device_flag}-{online:0|1}-{session_id}-{device_online_count}-{total_online_count}
```
The counts cover active sessions on the UID owner's current node only. They are not cluster-global presence and the event stream is neither complete nor ordered. A UID may contain `-`; parse the final five numeric fields from the right and treat the remaining prefix as the UID.
Receiver persistence and idempotency flow
Recommended receiver flow:
Suggested idempotency keys:
* `msg.notify`: `event + message_id`;
* `msg.offline`: `event + message_id + uid`, deduplicated per recipient;
* `user.onlinestatus`: deduplicate the complete string and treat it only as a local-session hint, never global online truth.
Production source authentication
The current HTTP sender sets only `Content-Type: application/json`; it **does not add a signature or shared-secret header**. Build a trusted boundary around it in production:
* use HTTPS;
* prefer a private network, service mesh, or egress proxy;
* enforce mTLS, fixed egress identity, or controlled credentials at the reverse proxy;
* restrict source IPs and request rates;
* never expose the callback as an anonymous public write endpoint;
* avoid logging complete sensitive payloads.
Queue limits, metrics, and failure handling
* `queue_size` is the bounded in-memory capacity for each event queue.
* `workers` controls concurrent sends per event queue.
* `msg_notify_batch_max_items` and its wait setting control message batches.
* `offline_uid_batch_size` controls offline UID chunking and compression.
* `retry_max_attempts` is the total number of attempts, not additional retries after the first.
If the receiver fails continuously, repair or isolate it instead of growing WuKongIM's memory queues without bound. Critical product state must remain reconstructable from the product database or message history.
Optional source fault test
Run from a source checkout containing this feature, with the Go toolchain required by `go.mod`. The test builds the current source and starts three real server processes plus a controllable callback on a loopback address. Docker, cloud servers, and an external Webhook URL are not required.
```bash
GOWORK=off go test -tags=e2e ./test/e2e/message/webhook \
-run '^TestBeforeSendWebhookAuthenticatedFaults$' -count=1 -timeout 2m -p=1 -v
```
This scenario enables client Token authentication and uses 256 hash slots, two Slot replicas, and two concurrent callbacks per node. The callback timeout is three seconds. Nodes intentionally use different failure policies to check denial, timeout-open, and error-open behavior separately; actual deployments must configure nodes consistently.
The test checks authentication, allowance, Payload replacement, business denial, a 100 ms slow response, timeout, HTTP 503, malformed JSON, redirects, and connection failure. With both callback slots occupied on one node, 16 additional sends must reject without invoking the callback. Another node must remain available, and the saturated node must recover after release. Received content must match committed history, denied messages must be absent, and callback counts and public metric deltas must match the decisions.
Verbose output records send latency, peak callback concurrency, and before/after heap and goroutine snapshots. If the platform lacks the process CPU metric, it reports `unavailable`. These observations describe a finite fault scenario, not throughput capacity, latency percentiles, or long-term memory stability. The test stops its processes and removes temporary data on completion.
The callback uses test rules and does not validate your business logic or Webhook signature authentication. Repeat the allow, replace, reject, and failure-policy cases against your actual business service when it is available.
Next: [release checks](/en/guide/integration/acceptance). Look up [event payloads](/en/api/webhooks/payloads) and [reliability boundaries](/en/api/webhooks/reliability-and-security) when implementing your receiver.
# Core Capabilities (/en/guide/product-overview/capabilities)
WuKongIM combines real-time connections, message persistence, offline recovery, and multi-device synchronization in one cluster. Applications organize messages for people, groups, or product-specific topics through channels, while the application backend retains control of identity, relationships, and product rules.
WuKongIM is designed for large-scale connection and messaging workloads, but there is no useful capacity number without hardware, storage, replica, and workload assumptions. Validate throughput, latency, and resource headroom with representative traffic.
## Capability map [#capability-map]
| Capability | Outcome and boundary |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Real-time access | Supports TCP WKProto, WebSocket, connection lifecycle, and bounded asynchronous processing. Boundary: A trusted application backend still establishes production identity, tokens, and product permissions. |
| Reliable messaging | Provides ordered messages for personal, group, and custom channels, with durable replication, retry deduplication, and offline synchronization. Boundary: Ordering is per channel; a successful write does not prove receipt, read, or business completion. |
| Multi-device state | Provides device sessions, presence, recent conversations, read positions, and unread state. Boundary: Application accounts and relationship data remain authoritative in the application backend. |
| Clustering and scale | Uses the same model for single-node and multi-node clusters, with 256 physical hash slots for routing, replicas, and failover. Boundary: Scale depends on channel distribution, hotspots, storage, and representative workload. |
| Extensions and operations | Provides webhooks, plugins, readiness, Prometheus, diagnostics, Manager, and command-line tools. Boundary: Webhooks and post-commit extensions are not durable queues; operational changes must respect safety boundaries. |
## Understanding message outcomes [#understanding-message-outcomes]
A default durable message is acknowledged after it reaches the corresponding durable commit boundary. Online delivery, offline synchronization, business processing, and read state happen later; none of those later outcomes can be inferred from "send succeeded" alone.
An integration should define separate tests for sent, delivered, read, and business-complete outcomes. See [Messaging Integration](/en/guide/integration/messaging) and [Message Flags](/en/api/dictionaries/message-flags) for precise acknowledgement, retry, and non-persistent behavior.
## Principles for scale [#principles-for-scale]
* Ordering is scoped to a channel, so unrelated channels do not share one global ordering bottleneck.
* Connections, message processing, and delivery use bounded queues and backpressure. Overload becomes visible as backlog or rejection instead of unbounded memory growth.
* Large groups, hot channels, and reconnect spikes need separate workload models; average QPS does not describe worst-case latency or recovery.
* Capacity results should record throughput, P99, error rate, queues, CPU, memory, disk, and network together. See [`wkcli bench`](/en/server/tools/wkbench) for the measurement workflow.
## What the application must provide [#what-the-application-must-provide]
WuKongIM does not replace account login, friendship or group rules, product authorization, content moderation, mobile push, the business database, or analytics. A trusted application backend issues credentials, maintains product relationships, calls protected server APIs, and interprets asynchronous events according to its own business outcomes.
# Product Overview (/en/guide/product-overview)
WuKongIM is open-source, self-hosted real-time messaging infrastructure. It lets applications provide chat, notifications, and multi-device message synchronization on their own infrastructure, and is designed for large-scale connection and messaging workloads.
WuKongIM v3 is still in beta. APIs, configuration, and storage formats may change before the stable release. Before production use, review [Upgrade & Migration](/en/server/operations/upgrade-and-migration), [Security & Permissions](/en/server/configuration/security), and [Health & Monitoring](/en/server/operations/health-and-monitoring).
## The problem it solves [#the-problem-it-solves]
A real-time messaging product needs more than a way to send data. It must manage many long-lived connections, per-channel message order, persistence, reconnect recovery, and multi-device state. WuKongIM provides these capabilities in one self-hosted cluster so that application teams can focus on their product and business rules.
An application or business service sends messages to a channel. A channel can represent a person, group, or product-specific topic. WuKongIM stores and synchronizes the messages, delivers them to online devices, and lets offline or reconnecting devices catch up later.
## Responsibility boundaries [#responsibility-boundaries]
| Participant | Primary responsibility |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| WuKongIM | Client connections, per-channel message order, persistence and replication, online delivery, offline recovery, and multi-device synchronization |
| Application backend | Accounts, credentials, friend or group relationships, product authorization, content governance, mobile push, and business data |
| Client SDK | Connection lifecycle, local message state, presentation, and user interaction |
## What it is not [#what-it-is-not]
* It is not a chat application with a user interface and complete product workflows.
* It does not replace the application backend or decide who may send which messages.
* It is not a general queue for offline jobs, global transactions, or long-term analytics.
## Is it a fit? [#is-it-a-fit]
WuKongIM is worth evaluating when a product needs many online users or devices, per-channel ordering, reconnect recovery, durable history, or multi-device synchronization. Chat, notifications, and customer service are the most direct fits; live interaction, IoT, and AI messaging workflows can use the same foundation.
If the requirement is only low-frequency background work, offline batch processing, or analytical queries, a task queue, stream processor, or analytical database is usually a better fit.
## One deployment model [#one-deployment-model]
Every WuKongIM deployment is a cluster. With one node, it is a **single-node cluster** that follows the same cluster paths as a multi-node deployment; there is no standalone mode that bypasses cluster semantics. Effective capacity depends on hardware, storage, replicas, channel distribution, and workload, so validate it with a representative scenario.
# Use Cases (/en/guide/product-overview/use-cases)
WuKongIM is best suited to products that need real-time connections, per-channel message order, durable history, reconnect recovery, or multi-device synchronization. It provides messaging infrastructure while the application retains identity, relationships, permissions, and complete business workflows.
## Core scenarios [#core-scenarios]
| Scenario | Capability and responsibility boundary |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Direct chat, group chat, and communities | WuKongIM: Personal and group channels, durable messages, online delivery, offline recovery, and multi-device synchronization. Application: Accounts, friend or group relationships, membership lifecycle, content governance, and mobile push. |
| Notifications and system messages | WuKongIM: System identities, durable messages, online delivery, and offline recovery. Application: Preferences, templates, rate limits, business receipts, and mobile push such as APNs or FCM. |
| Customer service | WuKongIM: Messages between visitors and agents, plus webhook or plugin extension points. Application: Waiting queues, assignment, tickets, quality controls, and sensitive-data governance. |
## Extended scenarios [#extended-scenarios]
| Scenario | Capability and responsibility boundary |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Live interaction | WuKongIM: Custom channels, real-time messages, and workload-validated online delivery. Application: Room lifecycle, gifts and abuse controls, degradation policy, and hotspot management. |
| IoT and signaling | WuKongIM: Stable user or device identities, bidirectional messaging, and reconnect recovery. Application: Device registry, command authorization, state shadow, timeout, and compensation. |
| AI messaging workflows | WuKongIM: Durable base messages, message events, and server-side extension points. Application: Model calls, flow control, content safety, cost, and durable final results. |
## Two common integration shapes [#two-common-integration-shapes]
### Client messaging product [#client-messaging-product]
The client SDK connects to WuKongIM, receives real-time messages, and restores offline messages. The application API manages accounts, relationships, permissions, and mobile push. Protected server APIs should not be exposed directly to untrusted clients.
### Server events and device communication [#server-events-and-device-communication]
A trusted service sends an event to a channel, and clients or devices consume it through long-lived connections and synchronization. Command authorization, timeouts, business results, and compensation remain application concerns; a successful message write does not mean that the business action completed.
## When to choose another system [#when-to-choose-another-system]
| Requirement | Usually a better fit |
| --------------------------------------------------------- | ------------------------------------------- |
| Low-frequency background work or scheduled jobs | Task queue |
| General cross-domain event streams and offline processing | Message queue or stream-processing platform |
| Long-term aggregation, search, and analytics | Analytical database or data platform |
These systems can complement WuKongIM. The distinction is that WuKongIM centers on users, devices, and channels that stay connected and exchange real-time messages.
## How to validate [#how-to-validate]
Start with the [Quick Start](/en/guide/quick-start) to run a single-node cluster and send the first message. During integration, read [Messaging Integration](/en/guide/integration/messaging). Before launch, complete [Integration Acceptance](/en/guide/integration/acceptance), then use [`wkcli bench`](/en/server/tools/wkbench) to validate capacity with a representative workload.
# Try the Four Demos (/en/guide/quick-start/chat-demo)
## 1. Launch together [#1-launch-together]
Install **Go 1.25+, Node.js 22.12+ (or 20.19+), and npm**. From the current repository root, run:
```bash
node demo/start.mjs
```
The launcher prepares a **single-node cluster with 256 hash slots** and the required business services, then opens the catalog. The first run downloads dependencies. The default mock model needs no key.
The catalog defaults to `http://127.0.0.1:5174/demos/` and uses a free port if occupied. **Use the printed or automatically opened address**; each Demo is already configured for this cluster. Press `Ctrl+C` to stop all processes from this run.
Choose a scenario; each Demo has a home link. The catalog shown uses Chinese labels. This capture comes from local main; entries in released images depend on their version.
## 2. Choose a scenario [#2-choose-a-scenario]
| Demo | First action | Explore |
| ------------------------ | ------------------------------------------------------------------- | -------------------------------------------------------- |
| Chat (即时聊天) | [Sign in as Alice and Bob](/en/guide/quick-start/first-message) | Direct/group chat, message edits, history recovery |
| Streaming replies (流式回复) | Create demo accounts, connect, and ask a question | Incremental replies, cancellation, failure, and recovery |
| Customer support (在线客服) | Start the demo, request human support, and accept it as an operator | AI reception, human takeover, multiple visitors |
| Agent | Start the demo, submit a task, and inspect tool cards | Tool calls, todo approval, pause, and cancellation |
These four Demos come from current source. **Starting a Docker container alone does not start the support or Agent Node business backends**. Use the launcher above for the complete experience. Source and independent launch instructions: [demo/README.md](https://github.com/WuKongIM/WuKongIM/blob/main/demo/README.md).
The launcher binds to loopback. Chat Demo credential creation writes the test UID's token; leave it off for existing accounts and migration acceptance. A trusted business backend must own production identities and permissions.
Existing Linux / Docker cluster: open Chat Demo
## Entry points [#entry-points]
| Environment | Chat Demo address | API base URL |
| ---------------------------------- | ------------------------------ | ----------------------- |
| Linux / Docker single-node cluster | `http://127.0.0.1:5001/demo/` | Same origin as the page |
| Repository three-node Compose | `http://127.0.0.1:15001/demo/` | Same origin as the page |
To connect to another API address, use:
```text
http://127.0.0.1:5001/demo/?apiurl=http://host:port
```
The Demo first calls `/route` to discover a client WebSocket address, then uses the JavaScript SDK to establish a long-lived connection.
## Troubleshoot connection addresses [#troubleshoot-connection-addresses]
Inspect the `/route` response in the browser Network panel. With default Docker port mappings and no `api.external_ws_addr`, `ws_addr` should use the API request hostname and Gateway port `5200`. Linux initialization listens on `127.0.0.1:5200`; SSH forwarding for both `5001` and `5200` allows the Demo to work without published-address overrides.
If the response still contains `ws://0.0.0.0:5200`, verify that the version includes automatic host completion and that no explicit published address contains a wildcard. On older versions, set `external_ws_addr = "ws://127.0.0.1:5200"` in the existing `[api]` section and restart; direct remote access requires a client-reachable server address and external port. `WK_EXTERNAL_WSADDR` overrides TOML.
If the page opens but WebSocket fails, check that Docker mappings or SSH forwarding include `5200`. Remapped ports, separate domains, and HTTPS/WSS proxies need explicit published addresses and a working TLS/WebSocket ingress; see [Docker Deployment](/en/server/deployment/docker).
## Login fields [#login-fields]
* **API base URL**: the WuKongIM HTTP API address.
* **Login account**: the unique user UID from the business system.
* **Login token**: the existing Web-device token. By default, the Demo connects without calling `/user/token` or changing server credentials.
* **Create or update demo credentials**: an explicit option for dedicated test accounts; it writes a Web slave-device credential through `/user/token`.
Credentials stay in this tab's `sessionStorage` and are never placed in the URL. Reloading or
choosing **Synchronize again** rebuilds SDK message, conversation, and cursor state before
synchronizing with the server. Logging out removes only the Demo credential entry.
The resynchronization action refuses to discard a nonempty draft; send or save it first.
Use existing-token mode for migration acceptance. This only resets the Demo's in-memory
cache; it does not clear another business app's database or certify its migration integration.
Connections with the same UID and primary-device flag may replace each other. For parallel tests, use different UIDs or independent browser sessions.
Production integration boundaries
## Production boundary [#production-boundary]
The Chat Demo calls `/user/token` directly only when demo credential creation is explicitly selected. Production clients must not register or reset their own tokens. A trusted business backend should validate identities and issue credentials.
A production integration also needs to:
* Configure TLS and network-access policy for HTTP, WebSocket, and TCP traffic.
* Replace example Manager accounts, JWT secrets, join tokens, and other fixed credentials.
* Enforce account, group, friend, authorization, and content rules in the business backend.
* Choose the appropriate client SDK and handle reconnects, offline synchronization, and local message state.
After two-way messaging succeeds, continue to the [next steps](/en/guide/quick-start/next-steps).
# Send the First Message (/en/guide/quick-start/first-message)
Complete the `/readyz` check in [Docker Deployment](/en/server/deployment/docker), or [launch all four Demos from source](/en/guide/quick-start/chat-demo). This is Bob's view in the current Chat Demo: Alice's message on the left, Bob's reply on the right. Click any image to enlarge it.
**① Received**: `hello from alice`. **② Reply**: `hello from bob`; Alice should receive it too.
## 1. Sign in as Alice [#1-sign-in-as-alice]
With Docker, open [Chat Demo](http://127.0.0.1:5001/demo/?lang=en). With the source launcher, choose Chat from the opened catalog. Enter this test identity:
| Field | Value |
| ------------ | ---------------------------------------------------------------------- |
| API base URL | Keep the automatic address; Docker defaults to `http://127.0.0.1:5001` |
| Account | `quickstart-alice` |
| Token | `alice-local-token` |
**① Keep the local API URL. ② Enter the UID and token. ③ Select the demo-credentials checkbox for this new test account, then choose Log in.**
The checkbox writes this UID's Web token. For existing accounts or migration acceptance, leave it off and enter the existing Web token. The default sign-in mode does not modify credentials.
## 2. Sign in as Bob and choose direct chat [#2-sign-in-as-bob-and-choose-direct-chat]
Open the Demo in another browser profile, private window, or browser. Use `quickstart-bob` / `bob-local-token`, selecting demo credential creation only for this new test account.
Wait until **both headers show Connected**. In Alice's page, click **+** above the chat list (or Start a chat), choose Direct chat, enter `quickstart-bob`, and click OK.
**① Select Direct chat. ② Enter the other user's UID. ③ Click OK.**
Does your released Docker UI look different?
These screenshots show the current local `main` Chat Demo. Earlier images may show Chinese labels: connection success (连接成功) and a top-right conversation selector (与谁会话?). Open that selector, choose direct chat (单聊), and enter the recipient UID. Test identities and messaging checks are the same. Use the [source launcher](/en/guide/quick-start/chat-demo) to try the four current Demos.
## 3. Exchange messages [#3-exchange-messages]
1. Alice sends `hello from alice`. Wait for the sending indicator to clear; in Bob's page, open a direct chat with `quickstart-alice` and confirm the same text appears.
2. Bob replies with `hello from bob`. Confirm Alice receives it.
## Success checklist [#success-checklist]
* Both UIDs are connected.
* Alice's send succeeds and Bob sees the original text.
* Bob's reply also appears on Alice's page.
Troubleshoot by symptom
| Symptom | Check first |
| --------------------------- | ----------------------------------------------------------------------- |
| Disconnected | `/readyz`, WebSocket port `5200`, and `docker logs --tail 100 wukongim` |
| Recipient sees no message | Both peers online, exact recipient UID, and direct-chat selection |
| Sessions replace each other | Different UIDs and independent browser sessions |
For Linux packages, check logs with `sudo journalctl -u wukongim -n 100 --no-pager`.
What does this verify?
The path covers demo token registration, exact-match CONNECT token checks, route discovery, WebSocket connection, channel message commit, and online delivery. Send success and recipient delivery are separate results; check both.
It verifies the development path. Production still needs account authentication, token expiry and replay policy, TLS, capacity, recovery, and durability validation. See [Chat Demo boundaries](/en/guide/quick-start/chat-demo).
Next: [choose an SDK and integrate your app](/en/sdk).
# Quick Start (/en/guide/quick-start)
Exchange messages between Alice and Bob on your computer. You need Docker and a browser; allow 5–10 minutes, excluding downloads.
To try the latest streaming, customer support, and Agent scenarios, [launch all four Demos from source](/en/guide/quick-start/chat-demo).
## Success checklist [#success-checklist]
* `/readyz` returns `{"ready":true}`.
* Both Alice and Bob show a successful connection.
* Each receives the other user's `hello from alice` or `hello from bob`.
These examples use test credentials. Before production, replace credentials and configure TLS, application authentication, backups, and recovery. See [Security & Access](/en/server/configuration/security).
Need to prepare Docker? See [Prerequisites](/en/guide/quick-start/prerequisites). Prefer installing directly on a Linux host? Follow the [Linux installation path](/en/guide/quick-start/single-node-cluster).
# Next Steps (/en/guide/quick-start/next-steps)
If readiness succeeds, both test UIDs connect, and messages flow in both directions, you have completed the quick verification.
## Choose a path [#choose-a-path]
## Frequently asked questions [#frequently-asked-questions]
### Is a single-node cluster a standalone mode? [#is-a-single-node-cluster-a-standalone-mode]
No. It is a cluster with one node and still uses the Controller, Slot, Channel, routing, and persistence paths.
### Can I use the example configuration in production? [#can-i-use-the-example-configuration-in-production]
No. At minimum, replace fixed credentials, plan TLS and access control, place data on managed durable storage, and validate capacity, failure recovery, and tail latency with the real workload.
### Why does data remain after a restart? [#why-does-data-remain-after-a-restart]
The Linux package stores node data in `/var/lib/wukongim/data`. Stopping the systemd service does not delete that directory.
### How do I change ports? [#how-do-i-change-ports]
Edit `/etc/wukongim/wukongim.toml`, run `sudo wukongim config validate --config /etc/wukongim/wukongim.toml`, then run `sudo systemctl restart wukongim`.
### Why must production clients not call `/user/token`? [#why-must-production-clients-not-call-usertoken]
It is a demo identity endpoint. A trusted business backend must issue or update production tokens after real identity and authorization checks.
### Where is the three-node environment? [#where-is-the-three-node-environment]
The repository-root `docker-compose.yml` provides a three-node development environment. It exposes development credentials and benchmark capabilities, so it is not a production template either.
# Prerequisites (/en/guide/quick-start/prerequisites)
## Try it on your computer [#try-it-on-your-computer]
* Install and start Docker; Docker Desktop is an option for macOS and Windows.
* Use a browser that can open two independent sessions.
* Keep local ports `5001`, `5100`, `5200`, and `5301` available.
```bash
docker version
```
The output should include both Client and Server. Next: [start a single-node cluster with Docker](/en/server/deployment/docker).
## Ports [#ports]
| Port | Purpose |
| ------ | ------------------------------------------- |
| `5001` | HTTP API, readiness, and Chat Demo |
| `5100` | Client TCP |
| `5200` | Client WebSocket |
| `5301` | Manager |
| `7001` | Node transport; no host port mapping needed |
The example API and Manager ports bind only to your computer. Keep management ports off the public internet; use the Docker guide's SSH tunnel for a remote server.
Without Docker: prepare a Linux host
Linux packages support `amd64` / `x86_64` on Ubuntu 24.04, Debian 13, Rocky Linux 9, AlmaLinux 9, or RHEL 9. You need `sudo`, `curl`, and access to `packages.githubim.com`; remote hosts also need SSH.
```bash
uname -m
. /etc/os-release && echo "$PRETTY_NAME"
curl --version
sudo -v
```
The package creates `/etc/wukongim`, `/var/lib/wukongim`, and `/var/log/wukongim`. Continue with the [Linux installation path](/en/guide/quick-start/single-node-cluster).
# Start a Single-node Cluster (/en/guide/quick-start/single-node-cluster)
Run all commands below on the Linux server.
## 1. Install WuKongIM [#1-install-wukongim]
On Ubuntu or Debian:
```bash
curl -fsSL https://packages.githubim.com/repo | sudo sh
sudo apt update
sudo apt install -y wukongim
```
On Rocky, AlmaLinux, or RHEL 9:
```bash
curl -fsSL https://packages.githubim.com/repo | sudo sh
sudo dnf -y --disablerepo='*' --enablerepo=wukongim-preview makecache --refresh
sudo dnf install -y wukongim
```
## 2. Initialize configuration [#2-initialize-configuration]
```bash
sudo wukongim config init --config /etc/wukongim/wukongim.toml
sudo wukongim config validate --config /etc/wukongim/wukongim.toml
```
Save the Manager administrator password printed by the first command. It is shown only once.
## 3. Start and check the cluster [#3-start-and-check-the-cluster]
```bash
sudo systemctl enable --now wukongim
curl --fail http://127.0.0.1:5001/readyz
```
A successful `/readyz` means that the single-node cluster can accept traffic. If it fails, inspect the logs:
```bash
sudo journalctl -u wukongim -n 100 --no-pager
```
## 4. Open the pages from your computer [#4-open-the-pages-from-your-computer]
For a remote Linux server, keep this SSH forwarding command running on your computer:
```bash
ssh -L 5001:127.0.0.1:5001 -L 5200:127.0.0.1:5200 -L 5301:127.0.0.1:5301 @
```
Then open these addresses in your computer's browser:
| Page | Address |
| --------- | ----------------------------- |
| Chat Demo | `http://127.0.0.1:5001/demo/` |
| Manager | `http://127.0.0.1:5301` |
The Manager username is `admin`; use the password saved during initialization.
To stop or start the service again:
```bash
sudo systemctl stop wukongim
sudo systemctl start wukongim
```
Data is stored in `/var/lib/wukongim/data`. Next: [send the first message](/en/guide/quick-start/first-message).
# AI & IoT Communication (/en/guide/tutorials/ai-and-iot)
Choose the outcome you want:
The default Gateway validates device CONNECT credentials; current WuKongIM HTTP API routes have no general product authentication. Authorize browsers, mobile clients, and firmware through the product backend. Keep model API keys, token writes, and management requests there.
## AI: streaming replies [#ai-streaming-replies]
Run the real-model Demo in [Agent Streaming Replies](/en/sdk/easy/agent-streaming), then integrate the frontend SDK and Agent backend. The server must contain online EVENT delivery; an older image with event endpoints may not support live push.
**Check:** Set the stream setting on the base; wait for a successful SENDACK before serial event writes. Use SDK push online. On reconnect, the BFF reads the final snapshot without polling history.
Increments may live only in a bounded Leader cache. After leadership changes, missing evidence makes finish fail closed. Replay complete deltas or the final snapshot, then retry. The full tutorial also covers cancellation and failures.
AI: full backend request for the base message
The product service first enforces model authorization, quotas, content safety, and cancellation policy. After a user prompt, send one base answer with a stable `client_msg_no` and the legacy stream bit. `setting=2` marks a stream message:
```bash
curl -sS http://127.0.0.1:5001/message/send \
-H 'Content-Type: application/json' \
-d '{
"from_uid":"ai-assistant",
"channel_id":"alice",
"channel_type":1,
"client_msg_no":"ai-answer-req-42",
"setting":2,
"payload":"eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiYWlfYW5zd2VyIiwic3RhdHVzIjoic3RyZWFtaW5nIiwicmVxdWVzdF9pZCI6InJlcS00MiJ9"
}'
```
Wait for `reason=1` before appending events. It proves that the base message reached Channel quorum commit, not that model generation, client rendering, or the product task completed.
The current product path verifies that the base is committed, has the stream setting, and matches the event identity. Preserve the exact Channel, sender, and `client_msg_no`; the base must not be NoPersist or SyncOnce. See [Agent Streaming Replies](/en/sdk/easy/agent-streaming) for the complete SDK and real-model tutorial.
AI: delta, idempotency, and finish requests
Open the default event lane:
```bash
curl -sS http://127.0.0.1:5001/message/event \
-H 'Content-Type: application/json' \
-d '{
"channel_id":"alice",
"channel_type":1,
"from_uid":"ai-assistant",
"client_msg_no":"ai-answer-req-42",
"event_id":"ai-answer-req-42-open",
"event_type":"stream.open",
"payload":{"kind":"text"}
}'
```
Give every generated chunk a new stable `event_id`. Retry an uncertain result with the same ID and identical payload:
```bash
curl -sS http://127.0.0.1:5001/message/event \
-H 'Content-Type: application/json' \
-d '{
"channel_id":"alice",
"channel_type":1,
"from_uid":"ai-assistant",
"client_msg_no":"ai-answer-req-42",
"event_id":"ai-answer-req-42-delta-0001",
"event_type":"stream.delta",
"payload":{"kind":"text","delta":"Hello"}
}'
```
Reusing an applied `event_id` returns its original result; it does not replace the event with a different payload. `stream.open`, `stream.delta`, and `stream.snapshot` may remain only in the bounded Slot-Leader cache, in which case `msg_event_seq` can be `0`.
Commit the terminal state when generation ends:
```bash
curl -sS http://127.0.0.1:5001/message/event \
-H 'Content-Type: application/json' \
-d '{
"channel_id":"alice",
"channel_type":1,
"from_uid":"ai-assistant",
"client_msg_no":"ai-answer-req-42",
"event_id":"ai-answer-req-42-finish",
"event_type":"stream.finish",
"payload":{"snapshot":{"kind":"text","text":"Hello"}}
}'
```
`stream.finish` places all still-open lanes and the reserved finish marker into one Slot proposal to create the terminal projection. If leadership changes and the new leader lacks cached evidence, finish fails closed. Replay the complete deltas or final snapshot before retrying finish instead of recording an incomplete answer as complete.
AI: terminal projection and recovery request
A reconnecting client can request compact event metadata:
```bash
curl -sS http://127.0.0.1:5001/channel/messagesync \
-H 'Content-Type: application/json' \
-d '{
"login_uid":"alice",
"channel_id":"ai-assistant",
"channel_type":1,
"start_message_seq":0,
"limit":20,
"pull_mode":1,
"event_summary_mode":"full"
}'
```
The response can include `event_meta`, completion state, and the final snapshot. A server containing online event delivery sends public increments to online Sessions as EVENT; EasySDK-JS receives them through `WKIMEvent.CustomEvent`. Do not poll history online; `/message/eventsync` is unnecessary. Offline recovery calls `/channel/messagesync` through the BFF. An older release with an event endpoint is not proof of live delivery support; see the deployment requirements in [Agent Streaming Replies](/en/sdk/easy/agent-streaming).
## IoT: send bounded telemetry [#iot-send-bounded-telemetry]
① Assign `device-42` a stable UID and revocable credentials. The backend creates `fleet-west` and authorizes the device and control service through membership policy.
**Check:** The device reports state and the control service independently receives it. Aggregate, sample, or send only changes; bound device rate, payload, and recovery windows. `reset=1` replaces members; reconcile an existing group against the product list first.
Telemetry: group setup, send, and capacity limits
Assign every device a stable UID and revocable credential policy. A group Channel must already exist, and the sending device must satisfy its membership policy. Create the product-owned telemetry Channel from a trusted service network:
```bash
curl -sS http://127.0.0.1:5001/channel \
-H 'Content-Type: application/json' \
-d '{
"channel_id":"fleet-west",
"channel_type":2,
"reset":1,
"subscribers":["device-42","control-service"]
}'
```
`reset=1` replaces membership with the supplied list, so use it for first-time provisioning or product-owned desired-state reconciliation, not blindly against unknown existing members. This example authorizes both the device and control service. After it succeeds, the device can send an ordinary durable message:
```bash
curl -sS http://127.0.0.1:5001/message/send \
-H 'Content-Type: application/json' \
-d '{
"from_uid":"device-42",
"channel_id":"fleet-west",
"channel_type":2,
"client_msg_no":"device-42-telemetry-1785897600",
"payload":"eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoidGVsZW1ldHJ5IiwidGVtcGVyYXR1cmVfYyI6MjEuNCwicmVwb3J0ZWRfYXQiOjE3ODU4OTc2MDB9"
}'
```
Do not write every high-frequency sample forever as a chat message. Aggregate by device and time window, sample, or emit only state changes. Bound per-device rate, payload size, Channel hotspots, webhook/plugin consumption, and offline recovery windows independently.
## IoT: send a recoverable command [#iot-send-a-recoverable-command]
② Bind the device with `/message/cmd/bind` before its first command, then send `sync_once=1` on the same source Channel. Binding starts after the current CMD-log tail and does not backfill older commands.
**Check:** On reconnect, use `/message/sync` to recover commands sent after binding, then advance the cursor with `/message/syncack`. Cursor advance does not prove execution; NoPersist has no offline recovery.
Commands: bind, send, sync, and unbind
A recoverable command should reuse a stable source Channel. Before sending the first target command, create durable CMD discovery for the device:
```bash
curl -sS http://127.0.0.1:5001/message/cmd/bind \
-H 'Content-Type: application/json' \
-d '{
"uid":"device-42",
"channel_id":"fleet-west",
"channel_type":2
}'
```
Binding starts at the sequence after the current CMD-log tail and does not backfill older commands, so it must happen before SEND. The trusted control service then sets `sync_once=1` on that same source Channel:
```bash
curl -sS http://127.0.0.1:5001/message/send \
-H 'Content-Type: application/json' \
-d '{
"from_uid":"control-service",
"channel_id":"fleet-west",
"channel_type":2,
"sync_once":1,
"client_msg_no":"device-command-op-42",
"payload":"eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiZGV2aWNlX2NvbW1hbmQiLCJvcGVyYXRpb25faWQiOiJvcC00MiIsImFjdGlvbiI6InNldF9mYW4iLCJ2YWx1ZSI6MiwiZGVhZGxpbmUiOjE3ODU4OTc2NjB9"
}'
```
It enters the separate CMD log, and the binding creates a UID-owned CMD discovery directory rather than an ordinary conversation. After reconnecting, the device synchronizes the latest command generation:
```bash
curl -sS http://127.0.0.1:5001/message/sync \
-H 'Content-Type: application/json' \
-d '{"uid":"device-42","limit":20}'
```
Call `/message/syncack` after processing that response. It advances the server-recorded latest CMD sync generation; the request `last_message_seq` is a required compatibility field, not proof of device execution. When the device permanently loses command access to this source Channel, call `/message/cmd/unbind` with the same binding request shape.
Request-scoped `subscribers` can provide bounded immediate targeting, but they do not automatically create CMD discovery membership and cannot directly promise reconnect recovery. An online-only command must set both `no_persist=1` and `sync_once=1`. It has no durable sequence, offline sync, ordinary conversation, or `msg.offline`. Ordinary online-only messages can use `no_persist=1, sync_once=0` and retain their source Channel, with the same lack of offline recovery.
## Close the loop with a business result [#close-the-loop-with-a-business-result]
③ Include `operation_id`, deadline, desired state, and version in the command. The device executes idempotently by `operation_id`, then sends a separate durable result message.
| Observation | What it confirms |
| ------------------------- | ------------------------------------------------------- |
| SENDACK / `reason=1` | Message commit |
| RECV / RECVACK / syncack | Online delivery, receive feedback, or cursor advance |
| Product execution receipt | Desired state and execution outcome under product rules |
**Check:** Retrying one command does not repeat execution. Apply product rules to expired commands; compare the actual device state with its result message.
Business receipts and launch checks
Every device command carries a product `operation_id`, deadline, desired state, and version. The device executes idempotently by `operation_id`, then sends a separate durable result message. SENDACK, RECV, RECVACK, and `/message/syncack` mean commit, online write, receive feedback, and cursor advance respectively; none proves that the fan reached the requested speed.
Before launch, test model cancellation, duplicate deltas, leader movement, cache pressure, terminal replay, device power loss, expired commands, repeated execution, out-of-order business results, hot device groups, and control-service rate limits. Continue with [Messages](/en/guide/core-concepts/messages), [Webhooks](/en/guide/integration/webhooks), and [Messaging](/en/guide/integration/messaging).
Continue with [Messaging](/en/guide/integration/messaging) and [Group membership](/en/guide/tutorials/large-groups).
# Direct Chat (/en/guide/tutorials/direct-chat)
Goal: Alice sends to Bob, Bob replies, and missing persistent messages recover through the chosen SDK path.
## 1. Connect both users [#1-connect-both-users]
Prepare a [single-node cluster](/en/server/deployment/docker) and two independent clients. Start with the [Web two-user example](/en/sdk/javascript/quickstart), or use the [Chat Demo](/en/guide/quick-start/first-message) for an initial exchange.
Your backend owns login, friendship, bans, and token issuance. Use stable UIDs and matching device categories. Protect WuKongIM HTTP API; enforce token expiry, rotation, and revocation as described in [Authentication](/en/guide/integration/authentication).
**Check:** both clients reach a connected, send-ready state. Run HTTP examples only from your trusted backend or protected test network.
Prepare test credentials from the backend
The product service owns account login, friendship, bans, and content policy. Choose stable UIDs for the two test users; do not substitute a nickname, connection ID, or device ID for the UID.
If the [Web two-user example](/en/sdk/javascript/quickstart) is running, its BFF has already prepared Alice and Bob; continue at step 2. For your own integration, store development tokens from the trusted backend. This example uses the Web device category `device_flag=1`; the client must use the same category:
```bash
curl -sS http://127.0.0.1:5001/user/token \
-H 'Content-Type: application/json' \
-d '{"uid":"alice","token":"alice-local-only","device_flag":1,"device_level":1}'
```
Store separate metadata for Bob, then connect both clients with their own UID, device identity, and token. If no SDK integration exists yet, use the [embedded Chat Demo](/en/guide/quick-start/first-message) to validate two browser sessions first.
## 2. Send to the peer UID [#2-send-to-the-peer-uid]
Alice sends to `channel_id=bob`, `channel_type=1`. Bob replies to `channel_id=alice`. Let the server normalize the internal person Channel ID.
Retain a stable `client_msg_no`. For an uncertain result, preserve the same logical-send identity and use the supported retry path. **Check:** inspect SENDACK or HTTP `reason=1` for the default persistent send; this does not prove reception or reading.
Backend send: text format and complete request
The client uses the **peer UID** as `channel_id` and sets `channel_type=1`. Do not construct or persist the internal canonical person Channel ID; the server normalizes it from sender and receiver UIDs.
A trusted product service can send with the same semantics:
This example uses the built-in SDK text format, matching the Web example. Before encoding, the JSON is:
```json
{"type":1,"content":"hello Bob"}
```
The `payload` below is the UTF-8 Base64 encoding of this JSON. Custom types and fields need a matching client decoder; see [Custom Messages](/en/sdk/javascript/advanced/custom-messages).
```bash
curl -sS http://127.0.0.1:5001/message/send \
-H 'Content-Type: application/json' \
-d '{
"from_uid":"alice",
"channel_id":"bob",
"channel_type":1,
"client_msg_no":"dm-alice-bob-0001",
"payload":"eyJ0eXBlIjoxLCJjb250ZW50IjoiaGVsbG8gQm9iIn0="
}'
```
Retain a stable, unique `client_msg_no`. If the network outcome is unclear, retry the same logical send with the same number instead of creating a duplicate with a new number. HTTP `reason=1` means the durable send reached durable Channel commit; it does not mean every Bob device received it.
## 3. Verify reception and reply [#3-verify-reception-and-reply]
Bob receives Alice's exact text once and replies; Alice independently verifies reception. Do not substitute the sender's success indicator for this check.
① Bob receives. ② Bob replies. The real Chat Demo capture uses the `quickstart-alice` / `quickstart-bob` test UIDs; it does not prove a read receipt. Click for the original.
## 4. Test disconnect and recovery [#4-test-disconnect-and-recovery]
Disconnect Bob, send a new persistent message from Alice, then reconnect Bob. Verify the missing message appears once, with the correct Channel order.
| SDK path | Recovery check |
| -------- | -------------------------------------------------------------------------------------------------------- |
| Full SDK | Synchronize missing persistent messages and merge/deduplicate online and recovered results |
| EasySDK | Verify online reconnect; history, conversations, and unread recovery require your backend implementation |
NoPersist has no durable history or offline recovery. `message_seq` orders one Channel, not all chats.
① SYNCED is the recovered message. ② Sync complete · 1 recovered is this run’s result. This is the full Web SDK example, not automatic EasySDK history sync. Click for the original.
Backend history query and merge rules
An online Bob should receive `RECV` and send `RECVACK` after processing it. Verify the durable log through the compatibility sync route; a person-Channel query still uses the peer UID:
```bash
curl -sS http://127.0.0.1:5001/channel/messagesync \
-H 'Content-Type: application/json' \
-d '{
"login_uid":"bob",
"channel_id":"alice",
"channel_type":1,
"start_message_seq":0,
"limit":20,
"pull_mode":1
}'
```
The response maps `channel_id` back to `alice`; `message_seq` is ordered only within this person Channel. Merge online delivery and reconnect sync by message ID, `client_msg_no`, and Channel sequence while tolerating duplicate arrival.
Account conversation and unread checks
```bash
curl -sS http://127.0.0.1:5001/conversation/list \
-H 'Content-Type: application/json' \
-d '{"uid":"bob","limit":20}'
```
Bob's item uses `channel_id=alice`. `unread` is computed from Bob's read position and visible messages in the Channel; it is not the total number of deliveries across devices. After the user confirms the current conversation is read, call through the trusted boundary:
```bash
curl -sS http://127.0.0.1:5001/conversations/clearUnread \
-H 'Content-Type: application/json' \
-d '{"uid":"bob","channel_id":"alice","channel_type":1}'
```
`clearUnread` advances Bob's read position (`read_seq`) to the newest committed ordinary message. Messages arriving during the operation can also have their unread badge cleared; this does not prove to Alice which messages Bob actually read. Other devices using the same UID see the update after synchronization.
Multiple devices and release scenarios
1. Connect a second Bob Session with a different `device_id`.
2. Define the product's primary/secondary-device conflict policy; one UID does not imply one connection.
3. Disconnect one device, then publish another durable message.
4. Reconnect and recover the missing sequence through SDK sync or `/channel/messagesync`.
5. Verify that online delivery, offline recovery, and account unread state are not treated as one completion signal.
Before launch, also test friendship removal, denylist policy, token revocation, disconnect retries, duplicate sends, direct-Channel distribution, and duplicate webhook consumption. Continue with [Groups & Large Groups](/en/guide/tutorials/large-groups) or [Messaging](/en/guide/integration/messaging).
Next: [Groups & Large Groups](/en/guide/tutorials/large-groups) and [release checks](/en/guide/integration/acceptance). Capture versions are recorded in the [Web tutorial](/en/sdk/javascript/quickstart).
# Tutorials (/en/guide/tutorials)
Choose an outcome, then check each tutorial's results.
Current WuKongIM HTTP API routes have no general product authentication. HTTP management requests belong only in a trusted product backend or a protected test environment.
**Verify separately:** Message commit, peer receipt, recovery, and business completion. Unread changes, provider acceptance, and command-sync cursors cannot replace business results.
Shared success model for all tutorials
The product service uses its own database, idempotency keys, and compensation flow when business results require strong consistency.
Continue with [Integration Architecture](/en/guide/integration/architecture) and [Release Checks](/en/guide/integration/acceptance).
# Groups & Large Groups (/en/guide/tutorials/large-groups)
Goal: Alice sends to `team-42`, Bob and Carol each receive it, then you verify the leave boundary.
## 1. Prepare the three-member group [#1-prepare-the-three-member-group]
Connect Alice, Bob, and Carol in independent clients with credentials from your backend. The backend creates stable group ID `team-42` with all three as ordinary subscribers.
Your product owns profiles, roles, approvals, moderation, and the membership database. **Membership and send HTTP routes belong behind a trusted backend:** clients call your business API, which checks roles before changing WuKongIM state.
**Check:** verify the exact three members through the authenticated, paginated [Manager](/en/server/operations/manager) subscriber query.
`reset=1` replaces the old member list. Use it for a fresh test group or a confirmed desired snapshot. Metadata writes, removal, insertion, and large-group refresh are not one transaction; after failure, retain successful progress and reconcile from the business snapshot.
Create the group: complete backend request
Choose a stable, non-recycled product group ID such as `team-42`. Create the Channel from a trusted service network and verify it with a small initial member set:
```bash
curl -sS http://127.0.0.1:5001/channel \
-H 'Content-Type: application/json' \
-d '{
"channel_id":"team-42",
"channel_type":2,
"reset":1,
"subscribers":["alice","bob","carol"]
}'
```
The compatible success response is `{"status":200}`. `channel_type=2` identifies a group Channel. This subscriber list does not replace the product membership database; the server uses it for messaging permission, synchronization, and delivery.
## 2. Send to the group ID [#2-send-to-the-group-id]
Alice uses `channel_id=team-42`, `channel_type=2`, a stable `client_msg_no`, and the SDK text format. The sender must satisfy current membership and send permissions.
**Check:** SENDACK or HTTP `reason=1` confirms the default message's durable commit. It does not prove reception by every member or completion of conversation synchronization.
Backend send and history query
The sender must satisfy current group permission and membership policy. A client SDK uses `channel_id=team-42` and `channel_type=2`; a trusted server-side example is:
This example uses the built-in SDK text format, matching the Web example. Before encoding, the JSON is:
```json
{"type":1,"content":"hello team"}
```
The `payload` below is the UTF-8 Base64 encoding of this JSON. Custom types and fields need a matching client decoder; see [Custom Messages](/en/sdk/javascript/advanced/custom-messages).
```bash
curl -sS http://127.0.0.1:5001/message/send \
-H 'Content-Type: application/json' \
-d '{
"from_uid":"alice",
"channel_id":"team-42",
"channel_type":2,
"client_msg_no":"team-42-0001",
"payload":"eyJ0eXBlIjoxLCJjb250ZW50IjoiaGVsbG8gdGVhbSJ9"
}'
```
`reason=1` means this Channel message was durably committed. It does not mean every member was online, every online Session write succeeded, or every client has completed its conversation-directory synchronization.
Bob can sync the group log:
```bash
curl -sS http://127.0.0.1:5001/channel/messagesync \
-H 'Content-Type: application/json' \
-d '{
"login_uid":"bob",
"channel_id":"team-42",
"channel_type":2,
"start_message_seq":0,
"limit":20,
"pull_mode":1
}'
```
`message_seq` defines one order inside the group. Member online delivery, RECVACK, unread state, and reconnect recovery remain separate observations.
## 3. Check both receivers [#3-check-both-receivers]
| Client | Expected result |
| -------------------- | -------------------------------------------------------------------------------------- |
| Bob | Receives `hello team`, shown once |
| Carol | Independently receives the same text, shown once |
| Reconnected receiver | Missing persistent messages recover through full-SDK sync or your backend history path |
`message_seq` orders this group only. Online delivery, RECVACK, unread, and recovery are separate checks; EasySDK does not automatically supply offline history or conversation recovery.
## 4. Verify removal [#4-verify-removal]
Commit the product membership decision, then remove Carol through the trusted subscriber route. Retain the operation result and reconcile partial failure. After the change converges, verify Carol's later send permissions and delivery behavior against your product policy.
**Check:** removal does not erase committed history or client caches. Define former-member history visibility and the starting point on rejoin.
Remove Carol: complete backend request
```bash
curl -sS http://127.0.0.1:5001/channel/subscriber_remove \
-H 'Content-Type: application/json' \
-d '{"channel_id":"team-42","channel_type":2,"subscribers":["carol"]}'
```
Add members and verify the desired membership
Add members:
```bash
curl -sS http://127.0.0.1:5001/channel/subscriber_add \
-H 'Content-Type: application/json' \
-d '{"channel_id":"team-42","channel_type":2,"subscribers":["dave","erin"]}'
```
Use `/channel/subscriber_remove` with the same request shape to remove members. Membership changes are de-duplicated and update each user's Channel relationship. The product service still needs the desired member list, version, and operation log so it can reconcile and compensate from successful progress after a failure.
WuKongIM HTTP API has no symmetric read route for ordinary subscribers, the denylist, or temporary subscribers. For strict verification, use the authenticated Manager `GET /manager/channels/:channel_type/:channel_id/subscribers` bounded paging query. WuKongIM HTTP API can read the allowlist, but that legacy route returns the whole list at once and is unsuitable for large-group inspection.
Leave and rejoin policy
The product service commits its membership decision, calls the trusted subscriber-removal route, and records a retryable task. Removal affects later permission and delivery plans, but it does not delete the existing Channel log or automatically define product database, client-cache, and historical-conversation policy.
Decide which historical sequence a former member may view, whether local records are removed, and where a returning member resumes. Those product rules cannot be inferred solely from “the subscriber row is gone.”
Advanced: reconcile 100,000 members in bounded batches
Do not place 100,000 UIDs into one `/channel` or `subscriber_add` JSON request. Use a product-owned coordinator:
* Use bounded application batches of hundreds to roughly one thousand UIDs, then tune from request bytes, latency, and error rate. Do not treat an internal chunk default as a permanent public API maximum.
* Each request is still chunked and de-duplicated internally, but neither the stages inside one request nor multiple HTTP requests form one global transaction. Preserve successful progress and repair the desired/observed difference after failure.
* `channel.large_group_subscriber_threshold` defaults to `500`; after an ordinary membership mutation, a count **greater than** the threshold refreshes the large-group marker. After changing the threshold, use controlled membership reconciliation and verification for existing Channels.
* After a large-group message is committed, the server pages through members, resolves online devices, and dispatches by destination node. Channel history retains one message instead of separate logs for all 100,000 members.
* SENDACK confirms durable commit without waiting for delivery to every receiving device. If the product needs “everyone processed it,” build a separate business acknowledgement and aggregation flow.
Advanced: capacity validation
Before launch, measure separately:
1. hot-group send rate, durable commit latency, and P99;
2. member paging, online-device lookup latency, and post-commit delivery backlogs;
3. per-node client write latency, disconnect ratio, and reconnect recovery;
4. membership update throughput and resuming progress after partial failures;
5. CPU, memory, disk, network, queue depth, rejection, and end-to-end tail latency.
Do not infer 100,000-member capacity from average QPS alone. Continue with [Channel Concepts](/en/guide/core-concepts/channels), [Cluster Configuration](/en/server/configuration/cluster), and [Benchmark Tooling](/en/server/tools/wkbench).
Next: [Channel Concepts](/en/guide/core-concepts/channels) and [release checks](/en/guide/integration/acceptance). Use [Benchmark Tooling](/en/server/tools/wkbench) to measure your actual target load.
# MQTT Quickstart (/en/guide/tutorials/mqtt)
Goal: Alice and Bob connect to MQTT, subscribe to their own inboxes, and exchange SDK text-format messages in both directions. Verify actual recipient content and stable server message identities.
Disabled by default; use a matching development candidate containing MQTT. Do not assume current released packages/images include it. Complete Linux, fault and load qualification remains outstanding. This tutorial runs in Node.js. Browsers use the separate MQTT WebSocket listener; see [Operations and Troubleshooting](/en/api/client-protocols/mqtt/operations-and-troubleshooting) for configuration.
## 1. Prepare a single-node cluster [#1-prepare-a-single-node-cluster]
You need Go 1.25.11, Node.js 20.11 or newer, npm, and source containing MQTT. See [Integration Architecture](/en/guide/integration/architecture) for responsibilities. This loopback development check retains single-node cluster semantics and 256 hash slots.
From the candidate source root:
```bash
go build -o ./bin/wukongim-mqtt ./cmd/wukongim
mkdir -p ./tmp/mqtt-quickstart
cp wukongim.toml.example ./tmp/mqtt-quickstart/wukongim.toml
WK_NODE_DATA_DIR=./tmp/mqtt-quickstart/data \
WK_MQTT_ENABLE=true \
WK_MQTT_LISTEN_ADDR=127.0.0.1:1883 \
WK_GATEWAY_TOKEN_AUTH_ON=true \
WK_GATEWAY_LISTENERS='[{"name":"tcp-wkproto","network":"tcp","address":"127.0.0.1:5100","transport":"gnet","protocol":"wkproto"}]' \
./bin/wukongim-mqtt -config ./tmp/mqtt-quickstart/wukongim.toml
```
Keep this terminal running and wait for startup. Check for configuration errors and that port 1883 is listening. The example WuKongIM HTTP API address is `127.0.0.1:5001`. If ports are occupied, change the configuration and client URL. See the [configuration reference](/en/server/configuration/reference) for MQTT fields.
## 2. Prepare credentials from the backend [#2-prepare-credentials-from-the-backend]
In another terminal at the source root, register development-only tokens. WuKongIM HTTP API lacks general product authentication and must not be exposed to untrusted clients. Production credentials are issued by your backend after authenticating login.
```bash
curl -sS http://127.0.0.1:5001/user/token \
-H 'Content-Type: application/json' \
-d '{"uid":"alice","token":"alice-local-only","device_flag":1,"device_level":1}'
curl -sS http://127.0.0.1:5001/user/token \
-H 'Content-Type: application/json' \
-d '{"uid":"bob","token":"bob-local-only","device_flag":1,"device_level":1}'
```
Check that both registration requests succeed. Each client uses its matching UID, token and `wk.device_flag="1"`. ClientID is not a password. See [Authentication and Topics](/en/api/client-protocols/mqtt/authentication-and-topics).
## 3. Install and run the complete example [#3-install-and-run-the-complete-example]
The source includes `docs-site/examples/mqtt-quickstart/quickstart.mjs`, pinned to MQTT.js 5.16.0. Install using its lockfile:
```bash
cd docs-site/examples/mqtt-quickstart
npm ci --no-audit --no-fund
MQTT_URL=mqtt://127.0.0.1:1883 \
MQTT_ALICE_TOKEN=alice-local-only \
MQTT_BOB_TOKEN=bob-local-only \
npm start
```
The clients install listeners before CONNECT, set `protocolVersion=5`, generate distinct ClientIDs per run, and use `clean=true` with Session Expiry 0. They publish only after both subscriptions succeed:
| Client | Inbox subscription | Publish target |
| ------ | ------------------------------ | -------------------------------------- |
| Alice | `wk/v1/users/YWxpY2U/messages` | Bob's `wk/v1/users/Ym9i/messages` |
| Bob | `wk/v1/users/Ym9i/messages` | Alice's `wk/v1/users/YWxpY2U/messages` |
Topic encoding is unpadded base64url. Payloads are UTF-8 JSON bytes such as `{"type":1,"content":"hello Bob"}`, without an extra MQTT envelope. Each publication carries an independent, stable `wk.client_msg_no`.
## 4. Check both directions [#4-check-both-directions]
Success produces JSON with `passed=true`, `client_version="5.16.0"`, and two `exchanges` containing Alice/Bob sender UIDs and decimal-string `message_id` / `message_seq`. It then disconnects normally and exits.
The example waits independently for successful PUBACK and actual recipient PUBLISH. It checks bytes, sender, application number and channel type without printing tokens or payloads. Numeric identities remain strings to avoid JavaScript precision loss. Successful PUBACK establishes server durable commit, not reading or business completion.
Subscription and publish acknowledgement waits are limited to 10 seconds, each receive wait to 20 seconds, and the complete run to 60 seconds. Failure exits nonzero. This verifies a first exchange, not performance. Production applications need their own processing, deduplication and reconnect strategy; see the [message contract](/en/api/client-protocols/mqtt/messages).
## 5. Cleanup and recovery checks [#5-cleanup-and-recovery-checks]
The program sends normal DISCONNECT and closes both connections without keeping offline sessions. After validation, press Ctrl+C in the server terminal to stop the development cluster. Do not delete live data.
A new run creates new sessions and does not establish offline recovery. Follow [Persistent Sessions and QoS](/en/api/client-protocols/mqtt/sessions-and-qos) with a stable ClientID, nonzero expiry and `clean=false`, then inspect Session Present.
## 6. Troubleshooting [#6-troubleshooting]
* Connection fails: inspect listener, version, UID/token and device category. This command uses raw TCP; browsers need a separate MQTT WebSocket listener and a `ws://` or `wss://` URL.
* Subscription fails: inspect own-inbox topic and base64url. Group subscriptions require existing membership.
* Exchange times out: inspect logs, permissions, quotas, recipient connection and ACKs. Preserve the original application key when investigating an uncertain result.
See [Operations and Troubleshooting](/en/api/client-protocols/mqtt/operations-and-troubleshooting). Next, read [HTTP / SDK Interoperability](/en/api/client-protocols/mqtt/interop) and [Will Messages](/en/api/client-protocols/mqtt/will). Client APIs are documented in the [official MQTT.js README](https://github.com/mqttjs/MQTT.js/blob/v5.16.0/README.md).
# Message Push (/en/guide/tutorials/push)
Goal: Alice receives an order notification while offline, then recovers the committed message when she opens the app.
WuKongIM emits offline candidates; the product backend calls APNs, FCM, or a vendor channel. Verify SENDACK, webhook HTTP `200`, provider acceptance, and user visibility separately.
## 1. Register devices and preferences [#1-register-devices-and-preferences]
The product backend stores UID, device, provider token, and authorization state. Update these on login, token refresh, logout, and invalidation receipts.
**Check:** A provider token differs from the CONNECT credential stored by `/user/token`. The product owns notification permissions, quiet hours, and display privacy.
Device data: complete fields and update points
At minimum, the product service stores stable UID, device ID, platform, provider token, token version, authorization state, locale, quiet hours, and the latest invalidation reason. `/user/token` stores a CONNECT credential that the default Gateway validates by UID and device category; it is not a push-provider token, and there is no current public token-read API for a push worker.
Production push therefore uses the product database as its source of truth and updates it during login, token refresh, logout, and provider invalidation callbacks. Never use a nickname or transient Session ID as the push identity.
## 2. Receive offline callbacks [#2-receive-offline-callbacks]
Enable `msg.offline`, then send Alice an ordinary durable message through a trusted backend. `reason=1` proves only that the message committed.
**Check:** Offline classification is per UID. Even one online route keeps Alice out of the offline list; pushing to each disconnected device requires a separate product policy.
**Callbacks have no built-in signature or shared-secret header.** Establish a trusted network or protected proxy first, then filter senders, service identities, and unauthorized users according to notification policy.
Enable callbacks: complete TOML configuration
Configure a protected product endpoint in `wukongim.toml`:
```toml
[webhook]
http_addr = "https://events.example.com/wukongim"
focus_events = ["msg.notify", "msg.offline"]
queue_size = 1024
workers = 16
offline_uid_batch_size = 512
request_timeout = "5s"
retry_max_attempts = 3
```
The current sender sets only `Content-Type: application/json`; it adds no signature or shared-secret header. Establish trust with HTTPS, a private network or service mesh, mTLS or fixed egress identity, body-size limits, and rate limits. See [Webhooks](/en/guide/integration/webhooks) for the complete configuration and receiver contract.
Send an order notification: full backend request
First provision a permissioned notification-service UID in the product. This example sends an ordinary durable message to Alice's person Channel. Its payload is an application message model, not a privileged server message type:
```bash
curl -sS http://127.0.0.1:5001/message/send \
-H 'Content-Type: application/json' \
-d '{
"from_uid":"notification-service",
"channel_id":"alice",
"channel_type":1,
"client_msg_no":"order-42-shipped-v1",
"payload":"eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoib3JkZXJfdXBkYXRlIiwidGl0bGUiOiJPcmRlciBzaGlwcGVkIiwiYm9keSI6IlRyYWNrIHBhY2thZ2UgaW4gdGhlIGFwcCIsIm9yZGVyX2lkIjoib3JkZXItNDIifQ=="
}'
```
`reason=1` means the message reached Channel quorum commit. Keep `client_msg_no` stable for safe retry after an uncertain result. Trusted system-UID bypass is optional and must not replace product authorization, rate limits, or notification preferences.
Offline candidates: payload, compressed UIDs, and message scope
If neither Alice nor `notification-service` has an online route when Presence is resolved, the product endpoint receives a result like this:
```text
POST /wukongim?event=msg.offline
Content-Type: application/json
```
```json
{
"header": {"no_persist": 0, "red_dot": 0, "sync_once": 0},
"message_id": 123456789,
"message_idstr": "123456789",
"client_msg_no": "order-42-shipped-v1",
"message_seq": 8,
"from_uid": "notification-service",
"channel_id": "",
"channel_type": 1,
"payload": "eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoib3JkZXJfdXBkYXRlIiwidGl0bGUiOiJPcmRlciBzaGlwcGVkIiwiYm9keSI6IlRyYWNrIHBhY2thZ2UgaW4gdGhlIGFwcCIsIm9yZGVyX2lkIjoib3JkZXItNDIifQ==",
"to_uids": ["notification-service", "alice"]
}
```
Offline candidates are collected before sender-echo suppression, so a person Channel's `to_uids` can include `from_uid`; the notification service is absent from the list when it has an online route. For a person message, webhook `channel_id` is the server's canonical Channel identity; product relationships still use the two UIDs and must not persist this internal value as their primary key. The receiver must also support `compress: "gzip"` with Base64 `compress_to_uids`. `msg.offline` applies only to ordinary durable messages: `SyncOnce`, request-scoped `subscribers`, and transient `NoPersist` do not produce this effect.
Offline classification is per UID, not per device. If Alice has even one online route, she is not in the offline UID list. A policy such as “push every disconnected device” must combine the product's device table with `msg.notify`, client receipts, and product-specific rules.
## 3. Persist before calling the provider [#3-persist-before-calling-the-provider]
Idempotently write a product outbox row keyed by `msg.offline + message_id + uid`; return HTTP `200` after commit. A background worker resolves active provider tokens, applies preferences, calls the provider, and records the outcome.
**Check:** Duplicate callbacks do not create duplicate tasks. Distinguish invalid tokens from retryable provider errors.
WuKongIM webhooks use bounded memory queues with no crash replay. The product outbox protects only persisted tasks; reconcile critical notifications against product events or durable message history.
Receiver and worker: complete processing order
The webhook receiver should quickly:
1. Authenticate the network source and event allowlist, and bound the body size.
2. Expand `to_uids` or decompress `compress_to_uids`, then filter `from_uid`, service identities, and unauthorized users according to product notification policy.
3. For every eligible UID, persist an outbox row keyed by `msg.offline + message_id + uid`.
4. Return HTTP 200 after the outbox commit.
5. Resolve active provider tokens by UID and apply quiet-hour, collapse, locale, and privacy rules in the background.
6. Call the provider, classify permanent token failures and retryable errors, and persist the outcome.
Webhook queues are node-local memory. Queue saturation, process exit, cancellation, or exhausted retries can drop an event, and a crash does not replay it. Critical push work must be reconstructible from a product outbox, a product event, or durable message history instead of relying on ever-larger WuKongIM queues.
## 4. Recover the message after opening [#4-recover-the-message-after-opening]
Push carries only a safe summary and product locator. When Alice opens the app, authenticate again and read the original message through SDK sync or a trusted backend's `/channel/messagesync`. The product implements history recovery separately for EasySDK.
**Check:** The notification opens the correct message, displayed once. Apply current product rules after logout, permission changes, or message retraction.
Recovery and launch checklist
A provider payload carries only a safe display summary and product locator, not the complete sensitive message. When the user opens it, the client authenticates again and reads the committed message through SDK sync or `/channel/messagesync`; the provider notification is never the message source of truth.
Before launch, test duplicate and missing webhooks, compressed UID lists, token rotation, partially online users, provider throttling, quiet hours, stale deep links, logout, and retracted content. Continue with [Webhooks](/en/guide/integration/webhooks) and [AI & IoT Communication](/en/guide/tutorials/ai-and-iot).
Continue with [Webhooks](/en/guide/integration/webhooks) and [Direct-chat recovery](/en/guide/tutorials/direct-chat#4-test-disconnect-and-recovery).
# Android API Reference (/en/sdk/android/api-reference)
This is a lookup page for common entry points, not a replacement for the task guides. The root singleton is `WKIM.getInstance()`.
## Initialization [#initialization]
| API | Purpose |
| ---------------------------- | ---------------------------------------------------- |
| `init(Context, uid, token)` | Initialize a user and open that UID's local database |
| `setDebug(boolean)` | Control debug logging |
| `setFileCacheDir(String)` | Set the file-cache directory |
| `getMsgManager()` | Message manager |
| `getConnectionManager()` | Connection manager |
| `getConversationManager()` | Conversation manager |
| `getChannelManager()` | Channel-profile manager |
| `getChannelMembersManager()` | Group-member manager |
## `ConnectionManager` [#connectionmanager]
| API | Purpose |
| ---------------------------------------------- | ------------------------------------------- |
| `addOnGetIpAndPortListener` | Supply a connection IP or hostname and port |
| `connection()` | Start a connection attempt |
| `disconnect(false)` | Stop connecting while retaining identity |
| `disconnect(true)` | Sign out and clear the token |
| `addOnConnectionStatusListener(key, listener)` | Observe connection state |
| `removeOnConnectionStatusListener(key)` | Remove a state observer |
States: `fail`, `success`, `kicked`, `syncMsg`, `connecting`, `noNetwork`, and `syncCompleted`.
## `MsgManager` [#msgmanager]
| API | Purpose |
| -------------------------------------------- | ------------------------------------------- |
| `send(content, channel)` | Send content to a channel |
| `sendWithOptions(content, channel, options)` | Send with advanced settings |
| `sendMessage(WKMsg)` | Send or retry an existing message |
| `addOnSendMsgCallback` | Observe local message insertion |
| `addOnSendMsgAckListener` | Observe the server send result |
| `addOnNewMsgListener` | Observe new messages |
| `addOnRefreshMsgListener` | Observe message-object changes |
| `getOrSyncHistoryMessages` | Read local and, when needed, remote history |
| `addOnSyncChannelMsgListener` | Connect remote channel history |
| `addOnUploadAttachListener` | Connect media upload |
| `registerContentMsg` | Register custom content |
Use the same key in the matching removal method, such as `removeNewMsgListener`, `removeSendMsgAckListener`, or `removeRefreshMsgListener`.
## `ConversationManager` [#conversationmanager]
| API | Purpose |
| ------------------------------- | ----------------------------------------- |
| `getAll(callback)` | Read the conversation list asynchronously |
| `getWithChannel` | Read the conversation for one channel |
| `updateRedDot` | Set a channel's unread count |
| `deleteWitchChannel` | Delete a conversation row |
| `addOnRefreshMsgListListener` | Observe batches of conversation changes |
| `addOnSyncConversationListener` | Connect multi-device conversation sync |
## `ChannelManager` [#channelmanager]
| API | Purpose |
| ----------------------------- | ----------------------------------- |
| `getChannel` | Read a local channel profile |
| `fetchChannelInfo` | Request a profile refresh |
| `addOnGetChannelInfoListener` | Connect user and group profile APIs |
| `saveOrUpdateChannel` | Store a profile locally |
| `addOnRefreshChannelInfo` | Observe profile changes |
## Common models [#common-models]
| Type | Meaning |
| ---------------------------------- | ------------------------------------------------------ |
| `WKChannel` | `channelID`, `channelType`, name, avatar, and settings |
| `WKMsg` | Message identity, content, sequence, and send state |
| `WKMessageContent` | Base class for message content |
| `WKTextContent` / `WKImageContent` | Built-in text and image content |
| `WKUIConversationMsg` | Chat-list row and unread count |
| `WKChannelMember` | Group-member profile |
Common channel types: `WKChannelType.PERSONAL` and `WKChannelType.GROUP`. Send states: `WKSendMsgResult.send_loading`, `send_success`, and `send_fail`.
# Android Channels (/en/sdk/android/channels)
A `WKChannel` answers “who should receive this message?” A direct channel ID is the other user's UID; a group channel ID is your product's group ID.
## Create and read channels [#create-and-read-channels]
```java
WKChannel person = new WKChannel("bob", WKChannelType.PERSONAL);
WKChannel group = new WKChannel("team-1", WKChannelType.GROUP);
WKChannel cached = WKIM.getInstance()
.getChannelManager()
.getChannel("bob", WKChannelType.PERSONAL);
```
`getChannel` returns `null` when there is no local profile. Use `channelID + channelType` as the complete identity; never cache by ID alone.
## Connect your user and group profile API [#connect-your-user-and-group-profile-api]
```java
ChannelManager manager = WKIM.getInstance().getChannelManager();
manager.addOnGetChannelInfoListener((channelID, channelType, callback) -> {
profileApi.fetch(channelID, channelType, channel -> {
callback.onResult(channel);
});
return null;
});
manager.fetchChannelInfo("bob", WKChannelType.PERSONAL);
```
The provider can immediately return a cached `WKChannel`, or return `null` and deliver an asynchronous network result through the callback. The result must contain the correct `channelID`, `channelType`, and `version`.
## Observe profile changes [#observe-profile-changes]
```java
manager.addOnRefreshChannelInfo("chat-header", (channel, isEnd) -> {
titleView.setText(
TextUtils.isEmpty(channel.channelRemark)
? channel.channelName
: channel.channelRemark
);
loadAvatar(channel.avatar);
});
```
When leaving the screen:
```java
manager.removeRefreshChannelInfo("chat-header");
```
## Group members [#group-members]
`ChannelMembersManager` manages group members. Connect your paginated member API through `addOnSyncChannelMembers`, then read members from the local manager. Large groups must be paginated; never load every member into the UI at once.
## Common problems [#common-problems]
* Only channel IDs appear: configure `addOnGetChannelInfoListener`, then call `fetchChannelInfo`.
* Name and avatar do not refresh: ensure the provider delivers a complete `WKChannel` and listener keys do not collide.
* Direct and group profiles get mixed: every cache key must include both `channelID` and `channelType`.
# Android Connection (/en/sdk/android/connection)
`ConnectionManager` owns the connection, heartbeat, and reconnection. Your app initializes the current user and returns a usable gateway endpoint when the SDK asks for one.
## Initialize the current user [#initialize-the-current-user]
```java
WKIM im = WKIM.getInstance();
im.setDebug(false);
im.init(applicationContext, uid, token);
```
`init` opens the local database for this UID. Pass an Application Context and call it only after application login. When switching accounts, log out the old account before initializing the new UID.
## Supply a gateway endpoint [#supply-a-gateway-endpoint]
Fixed endpoint:
```java
im.getConnectionManager().addOnGetIpAndPortListener(callback ->
callback.onGetSocketIpAndPort("im.example.com", 5100)
);
```
Dynamic routing:
```java
im.getConnectionManager().addOnGetIpAndPortListener(callback ->
gatewayApi.fetch(uid, token, (host, port) ->
callback.onGetSocketIpAndPort(host, port)
)
);
```
Do not add a URL scheme to the host. End your own loading state and retry with backoff when the application API fails; never block the main thread while waiting.
## Observe the full state sequence [#observe-the-full-state-sequence]
```java
String key = "app-connection";
private boolean syncInProgress = false;
im.getConnectionManager().addOnConnectionStatusListener(key, (status, reason) -> {
if (status == WKConnectStatus.connecting) {
showConnecting();
} else if (status == WKConnectStatus.syncMsg) {
syncInProgress = true;
showRestoringMessages();
} else if (status == WKConnectStatus.syncCompleted) {
showRestoreCompleted();
} else if (status == WKConnectStatus.success && syncInProgress) {
syncInProgress = false;
enableSending();
} else if (status == WKConnectStatus.kicked) {
syncInProgress = false;
requireLoginAgain(reason);
} else if (status == WKConnectStatus.noNetwork) {
syncInProgress = false;
showOffline();
} else if (status == WKConnectStatus.fail) {
syncInProgress = false;
showConnectionError(reason);
}
});
im.getConnectionManager().connection();
```
Listeners run on the main thread. Version `1.5.5` reports an initial `success`
before `syncMsg`; that first event is not send-ready. Enable sending only on the
later `success` after synchronization completes.
## Disconnect and log out [#disconnect-and-log-out]
```java
// Pause the connection while retaining current-account identity.
im.getConnectionManager().disconnect(false);
// Sign out: stop connecting, clear the token, and close the current database.
im.getConnectionManager().disconnect(true);
```
`disconnect(true)` finishes some cleanup asynchronously. Before quickly initializing another account, let sends for the old account reach a final state.
Remove the observer with:
```java
im.getConnectionManager().removeOnConnectionStatusListener(key);
```
## Common problems [#common-problems]
* No state callbacks: register the endpoint listener and call `connection()` after `init`.
* Repeated failures: verify the client persistent-connection port, not the HTTP API port.
* Duplicate callbacks: check whether the same screen registered under multiple keys.
# Android Conversations (/en/sdk/android/conversations)
`WKUIConversationMsg` is one row in the chat list. It contains a channel, last message, time, and unread count.
## Read the chat list asynchronously [#read-the-chat-list-asynchronously]
```java
WKIM.getInstance().getConversationManager().getAll(conversations -> {
adapter.submitList(conversations);
});
```
`getWkMsg()` returns the last message, `getWkChannel()` returns the local channel profile, and `lastMsgTimestamp` plus `unreadCount` can drive the list directly.
## Observe changes [#observe-changes]
```java
ConversationManager manager = WKIM.getInstance().getConversationManager();
manager.addOnRefreshMsgListListener("conversation-list", changedItems -> {
mergeByChannel(changedItems);
});
manager.addOnDeleteMsgListener("conversation-list", (channelID, channelType) -> {
removeRow(channelID, channelType);
});
```
The change callback can update only affected channels. For simpler code, asynchronously read `getAll` again after each callback.
## Unread counts and deletion [#unread-counts-and-deletion]
After the user opens a chat and sees its messages, set that channel's unread count to zero:
```java
manager.updateRedDot("bob", WKChannelType.PERSONAL, 0);
```
Delete one conversation row with:
```java
manager.deleteWitchChannel("bob", WKChannelType.PERSONAL);
```
The method is named `deleteWitchChannel` in this version. It removes the conversation row; it does not mean every local message for that channel is cleared.
## Multi-device conversation sync [#multi-device-conversation-sync]
```java
manager.addOnSyncConversationListener((lastMsgSeqs, msgCount, version, callback) -> {
conversationApi.sync(lastMsgSeqs, msgCount, version, callback::onBack);
});
```
The application API returns `WKSyncChat`, and the SDK notifies the conversation list after storing it. Keep Activity and Fragment work out of this provider.
Remove list and deletion observers when the screen is destroyed:
```java
manager.removeOnRefreshMsgListListener("conversation-list");
manager.removeOnDeleteMsgListener("conversation-list");
```
# Android (/en/sdk/android)
`WuKongIMAndroidSDK` is the full WuKongIM SDK for Android. It manages the persistent connection, message delivery, local message database, conversation list, and channel profiles. Use it when your app owns the chat UI.
These guides target `WuKongIMAndroidSDK 1.5.5` and use Java throughout.
## Before you start [#before-you-start]
Prepare:
* a reachable WuKongIM service;
* two test users, such as `alice` and `bob`;
* a `uid`, `token`, connection IP or hostname, and port returned by your application server;
* an Android application project.
Your application server should create the token and return it to an authenticated user. Never put a WuKongIM admin secret in the app or let the app call an admin API to create tokens.
## Recommended order [#recommended-order]
Read [Core Concepts](/en/sdk/wukongim/concepts) first if “channel” or “conversation” is unfamiliar. Open the [API Reference](/en/sdk/android/api-reference) when you only need a method name.
# Android Messages (/en/sdk/android/messages)
A message combines `WKMessageContent` with a destination `WKChannel`. `MsgManager` handles local storage, sending, receiving, and queries.
## Send to a direct or group chat [#send-to-a-direct-or-group-chat]
```java
WKTextContent text = new WKTextContent("Hello");
WKChannel person = new WKChannel("bob", WKChannelType.PERSONAL);
WKIM.getInstance().getMsgManager().send(text, person);
WKChannel group = new WKChannel("team-1", WKChannelType.GROUP);
WKIM.getInstance().getMsgManager().send(text, group);
```
`send` has no return value. Observe local insertion when you need the `WKMsg` immediately:
```java
msgManager.addOnSendMsgCallback("chat", message -> {
addPendingMessage(message.clientMsgNO, message);
});
```
## Observe incoming messages and send results separately [#observe-incoming-messages-and-send-results-separately]
```java
msgManager.addOnNewMsgListener("chat", messages -> {
for (WKMsg message : messages) {
appendIncomingMessage(message);
}
});
msgManager.addOnSendMsgAckListener("chat", message -> {
updateSendState(message.clientMsgNO, message.status);
});
```
Common send states:
| State | Meaning | Suggested UI |
| -------------------------------------------------- | ------------------------------------ | ------------------ |
| `WKSendMsgResult.send_loading` | Created and sending | Sending |
| `WKSendMsgResult.send_success` | Accepted by the server | Sent |
| `WKSendMsgResult.send_fail` | This send attempt failed | Retry action |
| `no_relation` / `black_list` / `not_on_white_list` | Product relationship rejects sending | Explain the reason |
Retry an existing message with:
```java
msgManager.sendMessage(failedMessage);
```
## Read local or remote history [#read-local-or-remote-history]
When opening a chat for the first time:
```java
msgManager.getOrSyncHistoryMessages(
"bob",
WKChannelType.PERSONAL,
0,
false,
0, // 0 reads toward older messages
30,
0,
new IGetOrSyncHistoryMsgBack() {
@Override public void onSyncing() {
showLoading();
}
@Override public void onResult(List messages) {
showMessages(messages);
}
}
);
```
When local data is incomplete, this method invokes the channel-history source you configured. See [Media & History](/en/sdk/android/advanced/media-and-history).
Remove every key used by the screen:
```java
msgManager.removeSendMsgCallBack("chat");
msgManager.removeNewMsgListener("chat");
msgManager.removeSendMsgAckListener("chat");
```
# Android Quickstart (/en/sdk/android/quickstart)
This page has one goal: exchange a text message between two online Android clients.
## Prerequisites [#prerequisites]
* Your application server returns a `uid`, `token`, gateway endpoint, and port for both `alice` and `bob`.
* You have two separate app processes or devices.
* Your Android project can access JitPack.
## 1. Install [#1-install]
Add JitPack to dependency repositories:
```groovy
maven { url 'https://jitpack.io' }
```
Pin the SDK in the app module:
```groovy
dependencies {
implementation 'com.github.WuKongIM:WuKongIMAndroidSDK:1.5.5'
}
```
## 2. Initialize and supply an endpoint [#2-initialize-and-supply-an-endpoint]
After application login, initialize with an Application Context:
```java
WKIM im = WKIM.getInstance();
im.setDebug(false);
im.init(getApplicationContext(), uid, token);
im.getConnectionManager().addOnGetIpAndPortListener(callback ->
callback.onGetSocketIpAndPort("im.example.com", 5100)
);
```
If an API assigns the gateway, request it asynchronously inside this listener and invoke `onGetSocketIpAndPort` on success.
## 3. Observe connection and messages [#3-observe-connection-and-messages]
```java
private static final String LISTENER_KEY = "chat-home";
private boolean syncInProgress = false;
im.getConnectionManager().addOnConnectionStatusListener(
LISTENER_KEY,
(status, reason) -> {
if (status == WKConnectStatus.syncMsg) {
syncInProgress = true;
Log.d("WKIM", "restoring messages");
} else if (status == WKConnectStatus.syncCompleted) {
Log.d("WKIM", "message restore complete");
} else if (status == WKConnectStatus.success && syncInProgress) {
syncInProgress = false;
Log.d("WKIM", "connected");
} else if (status == WKConnectStatus.kicked ||
status == WKConnectStatus.noNetwork) {
syncInProgress = false;
Log.e("WKIM", "connection interrupted: " + reason);
} else if (status == WKConnectStatus.fail) {
syncInProgress = false;
Log.e("WKIM", "connect failed: " + reason);
}
}
);
im.getMsgManager().addOnNewMsgListener(LISTENER_KEY, messages -> {
for (WKMsg message : messages) {
if (message.baseContentMsgModel instanceof WKTextContent) {
WKTextContent text = (WKTextContent) message.baseContentMsgModel;
Log.d("WKIM", message.fromUID + ": " + text.content);
}
}
});
im.getMsgManager().addOnSendMsgAckListener(LISTENER_KEY, message -> {
if (message.status == WKSendMsgResult.send_success) {
Log.d("WKIM", "message sent: " + message.clientMsgNO);
} else {
Log.e("WKIM", "message failed: " + message.clientMsgNO);
}
});
```
Give every observer a stable, unique key and remove it with the same key when its screen is destroyed.
## 4. Connect and send [#4-connect-and-send]
```java
im.getConnectionManager().connection();
```
Version `1.5.5` reports `success` once before synchronization starts and again
after synchronization finishes. Send only after the `success` that follows
`syncMsg` and `syncCompleted`:
```java
WKTextContent content = new WKTextContent("Hello, Bob");
WKChannel channel = new WKChannel("bob", WKChannelType.PERSONAL);
im.getMsgManager().send(content, channel);
```
## Expected result [#expected-result]
1. Alice and Bob both report the final successful connection after synchronization.
2. Alice's send-state observer receives `send_success`.
3. Bob's new-message observer prints “Hello, Bob”.
If connection fails, verify the endpoint, client port, and that the token belongs to this UID. If sending produces no result, make sure it happens after connection and another screen has not replaced the same listener key.
Clean up when the screen ends:
```java
im.getConnectionManager().removeOnConnectionStatusListener(LISTENER_KEY);
im.getMsgManager().removeNewMsgListener(LISTENER_KEY);
im.getMsgManager().removeSendMsgAckListener(LISTENER_KEY);
```
Continue with [Connection](/en/sdk/android/connection) and [Messages](/en/sdk/android/messages).
# Agent Streaming Replies (/en/sdk/easy/agent-streaming)
**One base message + incremental events = one continuously updated chat bubble.**
Use `easyjssdk@2.0.5` and complete the [Web Quickstart](/en/sdk/easy/javascript/getting-started). The server must include online EVENT delivery; verify support in older released images.
## 1. Understand the flow [#1-understand-the-flow]
| Location | Responsibility |
| --------------------- | -------------------------------------------------------------------- |
| Frontend SDK | Send questions, receive events, update bubbles |
| Product backend / BFF | Issue credentials, authorize access, expose history and cancellation |
| Agent backend | Hold model API keys, call the model, serialize event writes |
## 2. Run it [#2-run-it]
Start a WuKongIM cluster supporting this feature, then run the demo:
```bash
cd demo/streamdemo
npm ci
npm run build
npm start
```
Open `http://127.0.0.1:5175/streamdemo/` and select **真实模型 (Real model)** in settings:
| Setting | Value |
| ------------ | ---------------------------------------------------------------------------- |
| Model URL | Compatible Chat Completions `/v1` base or full `/chat/completions` URL |
| API key | Model service key |
| Model name | Leave blank to try `/models`; enter explicitly when discovery is unavailable |
| WuKongIM URL | Default `http://127.0.0.1:5001`; editable |
The demo uses a local proxy for model CORS. **Production keys and WuKongIM HTTP API calls stay on the backend.** [Run instructions](https://github.com/WuKongIM/WuKongIM/tree/main/demo/streamdemo)
## 3. Frontend: receive SDK events [#3-frontend-receive-sdk-events]
```bash
npm install --save-exact easyjssdk@2.0.5
```
The BFF supplies `bootstrap`. Implement both UI callbacks using the [receiver code](https://github.com/WuKongIM/WuKongIM/blob/main/demo/streamdemo/src/main.ts) as a reference. This example uses group Channel `2` with the user and Agent as members.
```ts
import { WKIM, WKIMEvent } from 'easyjssdk';
const im = WKIM.init(bootstrap.websocketUrl, {
uid: bootstrap.uid, token: bootstrap.token, deviceFlag: 1,
}, { singleton: false, debugLogging: false });
// On exit: use the same callback references.
function stop() {
im.off(WKIMEvent.Message, upsertMessage);
im.off(WKIMEvent.CustomEvent, applyStreamEvent);
im.destroy();
}
window.addEventListener('pagehide', stop, { once: true });
im.on(WKIMEvent.Message, upsertMessage);
im.on(WKIMEvent.CustomEvent, applyStreamEvent);
await im.connect();
await im.send(channelId, 2, { type: 1, content: 'Hello' });
```
| Callback | Merge rule |
| ------------------ | --------------------------------------------------------------------------------------------------------- |
| `upsertMessage` | Create the base bubble; reuse a bubble created by an earlier event |
| `applyStreamEvent` | Filter Channel, Agent UID, and lane first; update one reply by `client_msg_no` |
| Duplicates / gaps | Deduplicate `event_id`; merge `text_offset` in UTF-8 bytes; wait for a full snapshot when text is missing |
## 4. Backend: model deltas → message events [#4-backend-model-deltas--message-events]
This is the **normal-flow core**. `agent` is a connected backend SDK. Authorize the question and membership before creating an idempotent job. `chunks` comes from the [model SSE adapter](https://github.com/WuKongIM/WuKongIM/blob/main/demo/streamdemo/src/model.ts); configure the model URL, key, and name on the backend.
```ts
const clientMsgNo = job.replyClientMsgNo; // Persist once per reply.
const ack = await agent.send(channelId, 2, { type: 1, content: '' }, {
clientMsgNo, setting: { stream: true },
});
if (ack.reasonCode !== 1) throw new Error('Base message rejected');
await append('stream.open', { kind: 'text' });
let text = '';
for await (const delta of chunks) {
await append('stream.delta', { kind: 'text', delta }); // Serialize writes.
text += delta;
}
await append('stream.finish', { snapshot: { kind: 'text', text } });
```
Implement `append` on the backend: call [POST /message/event (OpenAPI reference)](/en/api/product-http/messages/appendMessageEvent), check HTTP and application results, and save the ID and payload before requesting. Reuse them on retries. Example request:
```json
{
"channel_id": "room-42", "channel_type": 2, "from_uid": "agent-42",
"client_msg_no": "reply-42", "event_id": "reply-42:2",
"event_key": "main", "event_type": "stream.delta", "visibility": "public",
"payload": { "kind": "text", "delta": "Hello" }
}
```
## 5. Finish and recover [#5-finish-and-recover]
| Scenario | Agent action | UI / history |
| ------------------ | ----------------------------------------------- | -------------------------------------- |
| Complete | `stream.finish` + full `snapshot` | Complete answer |
| User cancels | Abort model → `stream.cancel` → `stream.finish` | Partial text and cancelled state |
| Model fails | `stream.error` → `stream.finish` | Partial text and failed state |
| Uncertain IM write | Stop; reconcile and retry the original event | No blind new error or completion event |
**Online reception uses SDK push. History sync is for offline recovery: no polling or `/message/eventsync`.**
| Keep | Reason |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Persistent base with `stream: true` | Append only after successful SENDACK; no NoPersist / SyncOnce |
| Serialized writes and stable event IDs | Prevent reordering and repeated text |
| Complete terminal snapshot and independent write timeout | Live events may be lost; cancellation still needs finalization |
| Bounded buffers and full text | Buffer during recovery; replay a full snapshot before finish after Leader cache loss |
| `main` and `__finish__` | Finish uses the reserved lane; do not filter it out |
| Backend authorization and history filtering | Private events are not broadcast; visibility is not product authentication |
## 6. Full implementation and acceptance [#6-full-implementation-and-acceptance]
Check **normal reply → duplicate event → cancellation → truncated model stream → offline reconnect**. The same bubble must match history each time. After fixtures pass, verify your actual model.
API details: [Message Events](/en/api/product-http/messages/appendMessageEvent) · [History Sync](/en/api/product-http/messages/syncQuickstartChannelMessages) · [JSON-RPC](/en/api/client-protocols/json-rpc)
# Run official examples (/en/sdk/easy/examples)
Exchange messages between Alice and Bob before integrating the tutorial code into your application. Choose one platform below; the two clients may also use different platforms.
## 1. Prepare a development single-node cluster [#1-prepare-a-development-single-node-cluster]
Follow [Docker deployment](/en/server/deployment/docker) to start a development single-node cluster. Make WebSocket port `5200` and WuKongIM HTTP API port `5001` reachable. The default uses 256 hash slots. Keep the server running and check from a trusted development terminal:
```bash
curl -fsS http://127.0.0.1:5001/readyz
```
The readiness request should succeed. The default development URL is `ws://127.0.0.1:5200`; append `/ws` only when configured on the listener or proxy. Use WSS for production clients and keep management APIs within your trusted backend.
## 2. Prepare Alice and Bob [#2-prepare-alice-and-bob]
**Choose the device category first.** The requests below use mobile category `0`. Use `1` for Web and `2` for Rust/C#/C++/Python. Set each account’s value separately when platforms differ. `alice-token` and `bob-token` are local demonstration values only.
A trusted product backend should prepare each user's `uid`, `token`, and WebSocket URL. Only for a loopback development environment, a trusted terminal can call [`POST /user/token`](/en/api/product-http/users/setQuickstartUserToken) to create test identities. The commands below assume two native clients; change a user's `device_flag` to `1` when that user signs in through the Web example:
```bash
curl -fsS -X POST http://127.0.0.1:5001/user/token \
-H 'Content-Type: application/json' \
-d '{"uid":"alice","token":"alice-token","device_flag":0,"device_level":1}'
curl -fsS -X POST http://127.0.0.1:5001/user/token \
-H 'Content-Type: application/json' \
-d '{"uid":"bob","token":"bob-token","device_flag":0,"device_level":1}'
```
Native mobile clients use APP `0`, Web uses WEB `1`, and desktop uses PC `2`. The default product composition exactly validates the stored token for the same UID and device category. A production deployment must still protect this route, implement expiry and rotation policy, and separately prove rejection of invalid, revoked, and product-expired tokens.
Choose the WebSocket URL from where the client runs:
| Client | Local development URL |
| ------------------------------------------ | ------------------------------------------------------------------------- |
| Browser, Node.js, macOS app, iOS Simulator | `ws://127.0.0.1:5200` |
| Android Emulator | `ws://10.0.2.2:5200` |
| Physical phone | `ws://:5200`, with firewall and routing verified |
## 3. Choose and run an example [#3-choose-and-run-an-example]
Prepare the development tools for your platform. Source examples below use pinned versions. To integrate a distributed package into your application, use the install command in its quickstart.
### iOS [#ios]
```bash
git clone https://github.com/WuKongIM/WuKongEasySDK-iOS.git
cd WuKongEasySDK-iOS
git checkout v1.1.1
swift build -c release
cd Examples/WuKongIMExample-Unified
./build.sh macos
./build.sh ios
```
Boot an iOS Simulator, then run:
```bash
./build.sh ios --run
```
You can instead start the macOS variant with `./build.sh macos --run`. The iOS ATS cleartext exception in the example exists only for local `ws://` development; do not copy it into a production app.
[iOS quickstart](/en/sdk/easy/ios/getting-started)
### Android [#android]
```bash
git clone https://github.com/WuKongIM/WuKongEasySDK-Android.git
cd WuKongEasySDK-Android
git checkout v1.0.5
./gradlew :example:assembleDebug
./gradlew :example:installDebug
```
Launch the `example` on an API 21+ device or emulator. An Android Emulator reaches the host at `ws://10.0.2.2:5200`, not `localhost`. Use two devices or emulators for two native identities, or use the browser example as the peer.
[Android quickstart](/en/sdk/easy/android/getting-started)
### Flutter [#flutter]
```bash
git clone https://github.com/WuKongIM/WuKongEasySDK-Flutter.git
cd WuKongEasySDK-Flutter
git checkout v1.1.0
flutter pub get
cd example
flutter pub get
flutter devices
flutter run -d
```
Run every target you intend to ship. An iOS Simulator result cannot stand in for Android, Web, or desktop acceptance.
[Flutter quickstart](/en/sdk/easy/flutter/getting-started)
### Web [#javascript]
```bash
git clone https://github.com/WuKongIM/WuKongEasySDK-JS.git
cd WuKongEasySDK-JS
git checkout v2.0.5
npm ci
npm run build
python3 -m http.server 8080
```
Open `http://127.0.0.1:8080/example/`. Use two isolated browser contexts for Alice and Bob. Do not print tokens or complete payloads to the Console.
[Web quickstart](/en/sdk/easy/javascript/getting-started)
### Rust [#rust]
Requires Rust 1.86+. Check out source matching crates.io 0.1.0:
```bash
git clone --branch v0.1.0 --depth 1 https://github.com/WuKongIM/WuKongEasySDK-Rust.git
cd WuKongEasySDK-Rust
cargo build --locked --example chat
```
In two terminals set `WK_WS_URL`, `WK_UID`, `WK_TOKEN`, and `WK_PEER_UID`, then run `cargo run --locked --example chat`. Once both connect, type text and observe received events. Enter `/quit` to exit.
[Rust quickstart](/en/sdk/easy/rust/getting-started)
### C\# [#csharp]
Create `MyChat` with the complete `Program.cs` in the [C# quickstart](/en/sdk/easy/csharp/getting-started) and install public NuGet 1.0.0. Set `WUKONGIM_WS_URL`, `WUKONGIM_UID`, and `WUKONGIM_TOKEN` in each terminal:
```bash
# Alice
dotnet run --project MyChat -- bob
# Bob
dotnet run --project MyChat -- alice
```
After both print `Connected.`, press Enter to send and observe `Message received.` at the peer. Press Enter again after checking both directions to exit.
[C# quickstart](/en/sdk/easy/csharp/getting-started)
### C++ [#cpp]
Use the [C++ quickstart](/en/sdk/easy/cpp/getting-started) to build `my_app` with vcpkg, or use `wukong_chat` from a prebuilt archive. For `my_app`, set `WKIM_URL`, `WKIM_UID`, and `WKIM_TOKEN`, then run `./build/my_app bob` (Bob uses `alice`). For the archive client, set `WKIM_TOKEN` and run `./build/wukong_chat ws://127.0.0.1:5200 alice bob`.
With `my_app`, wait for both clients to connect, press Enter to send, then Enter again to exit. With `wukong_chat`, type messages interactively and `/quit` to exit. Windows executables are under `build/Release/` with an `.exe` suffix.
[C++ quickstart](/en/sdk/easy/cpp/getting-started)
### Python [#python]
Run in a Python 3.11+ virtual environment:
```bash
python -m pip install --index-url https://pypi.org/simple "wukong-easy-sdk==0.1.0"
git clone --branch v0.1.0 --depth 1 https://github.com/WuKongIM/WuKongEasySDK-Python.git
python WuKongEasySDK-Python/examples/chat.py
```
First set each terminal’s `WKIM_URL`, `WKIM_UID`, `WKIM_TOKEN`, and `WKIM_PEER`. Type text after both show `Connected`; enter `/quit` to exit.
[Python quickstart](/en/sdk/easy/python/getting-started)
## 4. Check results and exit [#4-check-results-and-exit]
1. Both clients show a successful connection.
2. Alice receives a server send result, and Bob observes Alice’s message event.
3. Bob replies and Alice receives it.
4. Exit using the platform instructions or close the page. Reopening should not leave duplicate events or an old connection.
Example interfaces may display test content; production logs must not record Tokens or full messages.
## Troubleshooting [#troubleshooting]
**Cannot connect:** check `/readyz`, the reachable address, and ports. Android Emulator uses `10.0.2.2`; physical devices use the development machine’s LAN address.
**Authentication fails:** check UID, Token, and `device_flag`. Use different UIDs for the two clients.
**Send succeeds but nothing arrives:** ensure the peer is online, the destination is its UID, and the message listener is registered. SENDACK does not prove recipient delivery.
**Messages are missing after being offline:** EasySDK does not fetch history. Recover through your backend or choose the full SDK.
## Next [#next]
[Choose a platform and integrate your application](/en/sdk/easy). Historical versions and independent source/package results are in [engineering validation records](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md). Continue testing WSS, Token rotation, and recovery in your deployment.
# WuKongEasySDK (/en/sdk/easy)
WuKongEasySDK provides lightweight connection, online messaging, and event APIs. Your application owns the backend and UI; the SDK authenticates with WebSocket JSON-RPC CONNECT and exchanges messages. Choose a platform below. Use [the official examples](/en/sdk/easy/examples) to prepare a server and test accounts.
## Choose a platform and send the first message [#choose-a-platform-and-send-the-first-message]
## Does it fit your application? [#does-it-fit-your-application]
| Choose EasySDK when you only need | Choose the full WuKongIMSDK when you also need |
| -------------------------------------------- | --------------------------------------------------------- |
| WebSocket connection and automatic reconnect | A local message database and offline recovery |
| Online messages in person or group Channels | Conversations, unread counts, and message synchronization |
| Send results and realtime message events | Push, multi-device behavior, and the broader platform API |
| Product-owned UI, persistence, and receipts | More client message state owned by the SDK |
EasySDK is neither a chat UI nor a complete product backend. A successful `send` means the server returned a send result; it does not mean the peer received, displayed, or processed the message. If you need the broader feature set, return to [SDK selection](/en/sdk).
Rust uses one `Client` per identity; clones share the socket. `subscribe()` returns a bounded receiver, `disconnect().await` permits later connection, and `destroy().await` closes permanently. The default device category is PC `2`. See the [Rust quickstart](/en/sdk/easy/rust/getting-started) for installation and lifecycle.
## Three concepts to start with [#three-concepts-to-start-with]
**Channel** is the message destination. A person message targets the other user’s UID; a group message targets a group ID created by your backend.
**Payload** is your application’s JSON content, such as `{"type":1,"content":"Hello"}`. Received content belongs in your UI or application handler.
**Send result** is the server’s processing result. It differs from recipient delivery and a read receipt. Check both the sender’s result and the recipient’s event during your first integration.
## Prepare connection credentials [#prepare-connection-credentials]
The client must not create its own identity or call WuKongIM HTTP API management routes. After product login, the trusted backend returns the minimum connection material over HTTPS:
```json
{
"uid": "alice",
"token": "short-lived-token",
"websocketUrl": "wss://im.example.com/ws"
}
```
| Field | Owner and constraint |
| -------------- | --------------------------------------------------------------------------------------------------------------- |
| `uid` | Stable user identity confirmed by the product backend; Alice and Bob use different values |
| `token` | Short-lived and revocable, scoped to this identity's connection, with no WuKongIM HTTP API management authority |
| `websocketUrl` | Selected by the backend from deployment configuration or a trusted routing result; production uses `wss://` |
Complete [Authentication & Tokens](/en/guide/integration/authentication) first. The default composition exactly matches each CONNECT token against the `/user/token` record for the same UID and device category. Your deployment must still protect that route, implement expiry and rotation policy, and prove that invalid, revoked, and product-expired tokens are rejected.
## Run Alice and Bob [#run-alice-and-bob]
All platforms follow the same minimal path:
1. Obtain separate `uid`, short-lived `token`, and `websocketUrl` values for Alice and Bob.
2. Create the clients in two independent devices, processes, or browser contexts by following the platform quickstart.
3. After both sides report a successful connection, have Alice send a text JSON payload to the person Channel `bob`.
4. Retain Alice's send result; on Bob, verify `fromUid`, the Channel, and the payload in the realtime message event.
5. Send from Bob to Alice and prove the reverse direction.
6. Leave the page or sign out, remove listeners, and disconnect; confirm there is no duplicate event or background connection.
This loop verifies online messaging only. Send acknowledgement, realtime receipt, and product completion are three different states; model them with [Messaging](/en/guide/integration/messaging).
To validate the environment first, follow [Run the Official Examples](/en/sdk/easy/examples) with the verified server and client revisions before moving tutorial code into your application.
## Release resources on exit [#release-resources-on-exit]
| Platform | Subscribe and unsubscribe | Connection owner exit |
| -------- | ------------------------------------------ | -------------------------------- |
| iOS | `onMessage` / `removeListener` | `disconnect()` |
| Android | `addEventListener` / `removeEventListener` | `disconnect()` |
| Flutter | `addEventListener` / `removeEventListener` | `disconnect()` + `dispose()` |
| Web | `on` / `off` | `destroy()` |
| Rust | `subscribe()` / drop receiver | `destroy().await` |
| C# | `+=` / `-=` | `await using` / `DisposeAsync()` |
| C++ | `on` / `off(listenerId)` | `destroy().get()` |
| Python | `on` / `off` | `async with` / `await destroy()` |
An application object usually owns the connection. Pages remove their own subscriptions; account logout releases the connection. Follow each platform’s singleton, threading, and reconnect rules.
## Versions and compatibility [#versions-and-compatibility]
| Platform | Tutorial version | Distribution |
| ------------------------------------------------------------ | ---------------- | --------------------------------- |
| [iOS](https://github.com/WuKongIM/WuKongEasySDK-iOS) | `1.1.1` | Swift Package Manager / CocoaPods |
| [Android](https://github.com/WuKongIM/WuKongEasySDK-Android) | `1.0.5` | Maven Central |
| [Flutter](https://github.com/WuKongIM/WuKongEasySDK-Flutter) | `1.1.0` | pub.dev |
| [Web](https://github.com/WuKongIM/WuKongEasySDK-JS) | `2.0.5` | npm |
| [Rust](https://github.com/WuKongIM/WuKongEasySDK-Rust) | `0.1.0` | crates.io |
| [C#](https://github.com/WuKongIM/WuKongEasySDK-CSharp) | `1.0.0` | NuGet |
| [C++](https://github.com/WuKongIM/WuKongEasySDK-CPP) | `0.1.0` | vcpkg / ZIP |
| [Python](https://github.com/WuKongIM/WuKongEasySDK-Python) | `0.1.0` | PyPI |
Tutorials pin the versions above for reproducibility and troubleshooting. Web 2.0.5 includes a handshake-failure reconnect repair; see its [release notes](https://github.com/WuKongIM/WuKongEasySDK-JS/releases/tag/v2.0.5). C++ uses the WuKongIM custom registry or a prebuilt archive matching your system.
The first task is online bidirectional messaging. Reconnect does not fetch offline history or automatically select another node. Cluster recovery and membership updates also depend on the server version. Exact historical environments, passing results, and failures remain in [engineering validation records](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md).
## Next [#next]
[Run an official example](/en/sdk/easy/examples) → follow your platform tutorial → check both message directions → use [production checks](/en/guide/integration/acceptance) for actual devices, WSS, Token rotation, and recovery.
# Flutter API reference (/en/sdk/flutter/api-reference)
This page is a quick lookup. Return to each task page for complete usage and call order. The SDK singleton is `WKIM.shared`.
## Initialization [#initialization]
| API | Purpose |
| ---------------------------------------- | ------------------------------------------------------------- |
| `setup(Options)` | Configure the current user and open that UID's local database |
| `Options.newDefault(uid, token, {addr})` | Create the default options |
| `Options.getAddr` | Supply a connection address dynamically |
| `connectionManager` | Connection and state |
| `messageManager` | Message sending, receiving, and queries |
| `conversationManager` | Conversation list and unread counts |
| `channelManager` | User and group metadata |
## `WKConnectionManager` [#wkconnectionmanager]
| API | Purpose |
| -------------------------------------- | ---------------------------------------------- |
| `connect()` | Start connecting |
| `disconnect(false)` | Disconnect while retaining the current account |
| `disconnect(true)` | Log out |
| `addOnConnectionStatus(key, listener)` | Observe connection state |
| `removeOnConnectionStatus(key)` | Remove the state listener |
States: `fail`, `success`, `kicked`, `syncMsg`, `connecting`, `noNetwork`, and `syncCompleted`.
## `WKMessageManager` [#wkmessagemanager]
| API | Purpose |
| ------------------------------------------- | --------------------------------------------------------- |
| `sendWithOption(content, channel, options)` | Send and await local processing |
| `sendMessage(content, channel)` | Send with default options |
| `addOnMsgInsertedListener` | Observe local insertion; one global callback |
| `addOnRefreshMsgListener(key, listener)` | Observe message-object and send-state changes |
| `addOnNewMsgListener(key, listener)` | Observe newly received messages |
| `getOrSyncHistoryMessages` | Read local history and request remote history when needed |
| `addOnSyncChannelMsgListener` | Connect a remote history API |
| `addOnUploadAttachmentListener` | Connect media upload |
| `registerMsgContent` | Register a custom payload |
Use the same key with `removeOnRefreshMsgListener` and `removeNewMsgListener` when a screen is disposed.
## `WKConversationManager` [#wkconversationmanager]
| API | Purpose |
| --------------------------------------------- | ----------------------------------------- |
| `getAll()` | Read all local conversations |
| `getAllUnreadCount()` | Read the total unread count |
| `getWithChannel(channelID, channelType)` | Read one channel's conversation |
| `updateRedDot(channelID, channelType, count)` | Update its unread count |
| `deleteMsg(channelID, channelType)` | Remove the conversation-list item |
| `addOnRefreshMsgListListener(key, listener)` | Observe conversation batches |
| `addOnSyncConversationListener` | Connect the multi-device conversation API |
## `WKChannelManager` [#wkchannelmanager]
| API | Purpose |
| ------------------------------------------ | ---------------------------------------- |
| `getChannel(channelID, channelType)` | Read local channel metadata |
| `fetchChannelInfo(channelID, channelType)` | Request a metadata refresh |
| `addOnGetChannelListener` | Connect your user and group metadata API |
| `addOrUpdateChannel` | Save metadata locally |
| `addOnRefreshListener(key, listener)` | Observe metadata changes |
## Common models [#common-models]
| Type | Meaning |
| ----------------------------------------- | ------------------------------------------------------------- |
| `WKChannel` | `channelID` and `channelType` together identify a chat target |
| `WKMsg` | Message identity, payload, sequence, and send state |
| `WKMessageContent` | Base class for message payloads |
| `WKTextContent` / `WKImageContent` | Built-in text and image payloads |
| `WKUIConversationMsg` | A conversation-list item and its unread count |
| `WKSyncConversation` / `WKSyncChannelMsg` | Sync results returned from your API to the SDK |
Common channel types are `WKChannelType.personal` and `WKChannelType.group`. Send states are `WKSendMsgResult.sendLoading`, `sendSuccess`, and `sendFail`.
# Flutter Channels (/en/sdk/flutter/channels)
A `WKChannel` answers “who should receive this message?” A direct channel ID is the other user's UID; a group channel ID is your product's group ID.
## Create and read channels [#create-and-read-channels]
```dart
final person = WKChannel('bob', WKChannelType.personal);
final group = WKChannel('team-1', WKChannelType.group);
final cached = await im.channelManager.getChannel(
person.channelID,
person.channelType,
);
```
The result is `null` when there is no local profile. Cache identity must include both `channelID` and `channelType`.
## Connect your user and group profile API [#connect-your-user-and-group-profile-api]
```dart
im.channelManager.addOnGetChannelListener(
(channelID, channelType, complete) async {
final profile = await profileApi.get(channelID, channelType);
final channel = WKChannel(channelID, channelType)
..channelName = profile.name
..channelRemark = profile.remark
..avatar = profile.avatar
..version = profile.version;
complete(channel);
},
);
im.channelManager.fetchChannelInfo('bob', WKChannelType.personal);
```
The provider must return the complete channel identity. It is one global slot and should be installed once at application scope.
## Observe changes [#observe-changes]
```dart
im.channelManager.addOnRefreshListener('chat-header', (channel) {
final title = channel.channelRemark.isNotEmpty
? channel.channelRemark
: channel.channelName;
updateHeader(title, channel.avatar);
});
```
Call `removeOnRefreshListener('chat-header')` when leaving the screen.
## Group members [#group-members]
`channelMemberManager` provides `getMembers`, `getMember`, and `saveOrUpdateList`. Fetch paginated members from your application server, write them through the manager, and update UI through keyed listeners. Large groups require pagination; never load every member at once.
## Common problems [#common-problems]
* Conversation has only an ID: configure the profile provider, then call `fetchChannelInfo`.
* Name does not refresh: ensure the provider invokes `complete` and page listener keys do not collide.
* Direct and group profiles get mixed: every cache key must include channel type.
# Flutter Connection (/en/sdk/flutter/connection)
`WKConnectionManager` owns the Flutter connection. First call `WKIM.shared.setup` to open the current user's local database, then supply a gateway endpoint and connect.
## Fixed and dynamic endpoints [#fixed-and-dynamic-endpoints]
Fixed `host:port`:
```dart
final options = Options.newDefault(uid, token, addr: 'im.example.com:5100')
..debug = false
..deviceFlag = 0;
await WKIM.shared.setup(options);
```
Resolve an endpoint before each connection:
```dart
final options = Options.newDefault(uid, token)
..debug = false
..getAddr = (complete) async {
final route = await gatewayApi.getAddress();
complete('${route.host}:${route.port}');
};
await WKIM.shared.setup(options);
```
`getAddr` must eventually invoke `complete`. The current parser splits on a colon, so pass a plain DNS/IPv4 `host:port` with no scheme.
## Understand the state sequence [#understand-the-state-sequence]
```dart
im.connectionManager.addOnConnectionStatus(
'app-connection',
(status, reasonCode, info) {
if (status == WKConnectStatus.connecting) {
showConnecting();
} else if (status == WKConnectStatus.success) {
showRestoringConversations();
} else if (status == WKConnectStatus.syncMsg) {
showRestoringConversations();
} else if (status == WKConnectStatus.syncCompleted) {
enableSending();
} else if (status == WKConnectStatus.kicked) {
requireLoginAgain();
} else if (status == WKConnectStatus.noNetwork) {
showOffline();
} else if (status == WKConnectStatus.fail && reasonCode != null) {
showConnectionError(reasonCode);
}
},
);
```
`connect()` first calls `disconnect(false)`, so it can emit a `fail` with no `reasonCode` during local cleanup. That is not a server rejection. A normal sequence is `connecting → success → syncMsg → syncCompleted`.
The conversation provider must complete its callback or state remains at `syncMsg`. A real app returns a complete `WKSyncConversation` from its application server.
## Disconnect and log out [#disconnect-and-log-out]
```dart
// Stop connecting while retaining current-user configuration.
im.connectionManager.disconnect(false);
// Sign out: clear UID/token, fail pending sends, and close the local database.
im.connectionManager.disconnect(true);
```
Remove the state observer with:
```dart
im.connectionManager.removeOnConnectionStatus('app-connection');
```
## Common problems [#common-problems]
* Stuck at `syncMsg`: the conversation provider did not call its completion function.
* Address range error: the address is not a simple `host:port`.
* Duplicate page callbacks: one feature registered under multiple keys or did not remove its key during teardown.
# Flutter Conversations (/en/sdk/flutter/conversations)
`WKUIConversationMsg` is one row in the chat list. It contains a channel, last message, time, and unread count, stored in the current UID's sqflite database.
## Read the list [#read-the-list]
```dart
final conversations = await im.conversationManager.getAll();
setState(() => items = conversations);
```
Read the last message with `await item.getWkMsg()` and its channel profile with `await item.getWkChannel()`.
## Observe changes [#observe-changes]
```dart
im.conversationManager.addOnRefreshMsgListListener(
'conversation-list',
(changedItems) => mergeByChannel(changedItems),
);
im.conversationManager.addOnDeleteMsgListener(
'conversation-list',
(channelID, channelType) => removeRow(channelID, channelType),
);
```
For a small list, re-reading `getAll()` after each change is also reasonable and simpler.
## Unread counts and deletion [#unread-counts-and-deletion]
```dart
await im.conversationManager.updateRedDot(
'bob',
WKChannelType.personal,
0,
);
final total = await im.conversationManager.getAllUnreadCount();
await im.conversationManager.deleteMsg('bob', WKChannelType.personal);
```
Deleting a conversation row does not clear every message in that channel.
## Connect server synchronization [#connect-server-synchronization]
```dart
im.conversationManager.addOnSyncConversationListener(
(lastMsgSeqs, msgCount, version, complete) async {
final result = await conversationApi.sync(
lastMsgSeqs: lastMsgSeqs,
msgCount: msgCount,
version: version,
);
complete(toWKSyncConversation(result));
},
);
```
This provider is one global slot with no page-scoped removal method, so install it once through an application service. Finish your request for success, empty data, and failure; otherwise connection stays at `syncMsg`.
Remove keyed listeners when the page is destroyed.
# Flutter (/en/sdk/flutter)
`wukongimfluttersdk` is the full WuKongIM SDK for Flutter. It manages the persistent connection, message delivery, local sqflite data, conversation list, and channel profiles for Flutter apps that own their chat UI.
These guides target `wukongimfluttersdk 1.7.9` and use Dart throughout. The package depends on `dart:io` and sqflite, so it is not for Flutter Web.
## Before you start [#before-you-start]
Prepare:
* a reachable WuKongIM service;
* two test users, such as `alice` and `bob`;
* a `uid`, `token`, and `host:port` returned by your application server;
* a Flutter project with its Android or iOS native configuration complete.
Your application server should create the token and return it to an authenticated user. Never put a WuKongIM admin secret in the app or let the app call an admin API to create tokens.
## Recommended order [#recommended-order]
Read [Core Concepts](/en/sdk/wukongim/concepts) first if “channel” or “conversation” is unfamiliar. Open the [API Reference](/en/sdk/flutter/api-reference) when you only need a method name.
# Flutter Messages (/en/sdk/flutter/messages)
A message combines `WKMessageContent` with a destination `WKChannel`. `WKMessageManager` handles local storage, sending, receiving, and queries.
## Send to a direct or group chat [#send-to-a-direct-or-group-chat]
```dart
await im.messageManager.sendWithOption(
WKTextContent('Hello'),
WKChannel('bob', WKChannelType.personal),
WKSendOptions(),
);
await im.messageManager.sendWithOption(
WKTextContent('Hello everyone'),
WKChannel('team-1', WKChannelType.group),
WKSendOptions(),
);
```
Prefer awaiting `sendWithOption` directly. Although `sendMessage` is declared async in this version, it does not await its internal send flow.
## Observe local insertion, send state, and new messages [#observe-local-insertion-send-state-and-new-messages]
```dart
// This is one global callback with no key or removal API. Install it once
// through an application-scoped dispatcher.
im.messageManager.addOnMsgInsertedListener((message) {
addPendingMessage(message.clientMsgNO, message);
});
im.messageManager.addOnRefreshMsgListener('chat', (message) {
updateSendState(message.clientMsgNO, message.status);
});
im.messageManager.addOnNewMsgListener('chat', (messages) {
appendIncomingMessages(messages);
});
```
Send states:
| State | Meaning | Suggested UI |
| --------------------------------------------- | ------------------------------------ | ------------------ |
| `WKSendMsgResult.sendLoading` | Stored locally and sending | Sending |
| `WKSendMsgResult.sendSuccess` | Accepted by the server | Sent |
| `WKSendMsgResult.sendFail` | This send attempt failed | Retry action |
| `noRelation` / `blackList` / `notOnWhiteList` | Product relationship rejects sending | Explain the reason |
An existing failed message can be sent through the connection manager again, but product code should first confirm that it still belongs to the current account and channel.
## Read messages [#read-messages]
When opening a chat for the first time:
```dart
im.messageManager.getOrSyncHistoryMessages(
'bob',
WKChannelType.personal,
0,
false,
0, // Read toward older messages.
30,
0,
(messages) => showMessages(messages),
() => showSyncing(),
);
```
When local data is incomplete, the SDK calls your remote-history provider. See [Media & History](/en/sdk/flutter/advanced/media-and-history).
On page teardown, call `removeOnRefreshMsgListener('chat')` and `removeNewMsgListener('chat')`.
# Flutter Quickstart (/en/sdk/flutter/quickstart)
This page has one goal: exchange a text message between two online Flutter clients.
## Prerequisites [#prerequisites]
* Your application server returns a `uid`, `token`, and `host:port` for both `alice` and `bob`.
* You have two separate app processes or devices. `WKIM.shared` is a process singleton.
* Your Flutter project runs on the target device.
## 1. Install [#1-install]
Pin the version in `pubspec.yaml`:
```yaml
dependencies:
wukongimfluttersdk: 1.7.9
```
Then run:
```bash
flutter pub get
```
## 2. Initialize [#2-initialize]
```dart
import 'package:flutter/widgets.dart';
import 'package:wukongimfluttersdk/common/options.dart';
import 'package:wukongimfluttersdk/entity/channel.dart';
import 'package:wukongimfluttersdk/entity/conversation.dart';
import 'package:wukongimfluttersdk/entity/msg.dart';
import 'package:wukongimfluttersdk/model/wk_text_content.dart';
import 'package:wukongimfluttersdk/type/const.dart';
import 'package:wukongimfluttersdk/wkim.dart';
WidgetsFlutterBinding.ensureInitialized();
final im = WKIM.shared;
final options = Options.newDefault(
bootstrap.uid,
bootstrap.token,
addr: bootstrap.address, // For example, im.example.com:5100
)
..debug = false
..deviceFlag = 0;
final initialized = await im.setup(options);
if (!initialized) {
throw StateError('WuKongIM local database initialization failed');
}
```
The address must not include `tcp://`, `http://`, or `https://`.
## 3. Supply conversation sync and observe state [#3-supply-conversation-sync-and-observe-state]
After the connection protocol succeeds, the SDK synchronizes recent conversations. Even a new test account must finish this step:
```dart
im.conversationManager.addOnSyncConversationListener(
(lastMsgSeqs, msgCount, version, complete) async {
// A real app calls its server and returns a complete WKSyncConversation.
final empty = WKSyncConversation()
..uid = bootstrap.uid
..conversations = [];
complete(empty);
},
);
const listenerKey = 'chat-home';
im.connectionManager.addOnConnectionStatus(
listenerKey,
(status, reasonCode, info) {
if (status == WKConnectStatus.syncCompleted) {
print('WuKongIM ready');
} else if (status == WKConnectStatus.fail && reasonCode != null) {
print('connect failed: $reasonCode');
} else if (status == WKConnectStatus.kicked) {
print('this account was signed in elsewhere');
}
},
);
```
`WKConnectStatus.success` means the connection protocol succeeded. It is followed by `syncMsg` and `syncCompleted`; waiting for `syncCompleted` is the simplest send gate.
## 4. Observe messages and connect [#4-observe-messages-and-connect]
```dart
im.messageManager.addOnNewMsgListener(listenerKey, (messages) {
for (final message in messages) {
final content = message.messageContent;
if (content is WKTextContent) {
print('${message.fromUID}: ${content.content}');
}
}
});
im.messageManager.addOnRefreshMsgListener(listenerKey, (message) {
if (message.status == WKSendMsgResult.sendSuccess) {
print('message sent: ${message.clientMsgNO}');
} else if (message.status == WKSendMsgResult.sendFail) {
print('message failed: ${message.clientMsgNO}');
}
});
im.connectionManager.connect();
```
## 5. Send text [#5-send-text]
After `syncCompleted`, send from Alice to Bob:
```dart
await im.messageManager.sendWithOption(
WKTextContent('Hello, Bob'),
WKChannel('bob', WKChannelType.personal),
WKSendOptions(),
);
```
Awaiting `sendWithOption` ensures the local storage step is complete. The server result still comes through the message refresh listener.
## Expected result [#expected-result]
1. Alice and Bob both reach `syncCompleted`.
2. Alice's message becomes `sendSuccess`.
3. Bob's new-message listener prints “Hello, Bob”.
Clean up when the screen ends:
```dart
im.connectionManager.removeOnConnectionStatus(listenerKey);
im.messageManager.removeNewMsgListener(listenerKey);
im.messageManager.removeOnRefreshMsgListener(listenerKey);
```
Continue with [Connection](/en/sdk/flutter/connection) and [Messages](/en/sdk/flutter/messages).
# HarmonyOS API reference (/en/sdk/harmonyos/api-reference)
This page is a quick lookup. Return to each task page for complete usage and call order. The SDK singleton is `WKIM.shared`.
## Imports and initialization [#imports-and-initialization]
The package root exports only `WKIM`:
```typescript
import { WKIM } from '@wukong/wkim'
```
Import models and constants from `@wukong/wkim/src/main/ets/entity/Bean`, and payloads from `@wukong/wkim/src/main/ets/model/...`.
| API | Purpose |
| -------------------------------------- | ------------------------------------------------------------------- |
| `init(uid, token, address?, context?)` | Configure the user and open that UID's local database |
| `config.provider` | Application implementations for routing, sync, metadata, and upload |
| `connectionManager()` | Connection and state |
| `messageManager()` | Message sending, receiving, and queries |
| `conversationManager()` | Conversation list and unread counts |
| `channelManager()` | User and group metadata |
| `channelMemberManager()` | Group-member metadata |
## Common providers [#common-providers]
| Field | Purpose |
| ------------------------------- | ------------------------------------------------------------- |
| `connectAddrCallback` | Return a dynamic `host:port` |
| `syncConversationCallback` | Synchronize conversations, recent messages, and unread counts |
| `syncMessageCallback` | Synchronize history for one channel |
| `channelInfoCallback` | Fetch user or group metadata |
| `channelMemberCallback` | Incrementally synchronize group members |
| `channelMemberWithPageCallback` | Page or search group members |
| `uploadAttachmentCallback` | Upload media and set its URL |
Providers are process-global fields; assigning one again replaces its previous implementation.
## `ConnectionManager` [#connectionmanager]
| API | Purpose |
| --------------------------------------- | ---------------------------------------------- |
| `connection()` | Start connecting |
| `disConnection(false)` | Disconnect while retaining the current account |
| `disConnection(true)` | Log out and close the local database |
| `addConnectStatusListener(listener)` | Observe connection state |
| `removeConnectStatusListener(listener)` | Remove it with the same function object |
States: `fail`, `success`, `kicked`, `syncing`, `connecting`, `noNetwork`, and `syncCompleted`.
## `MessageManager` [#messagemanager]
| API | Purpose |
| ------------------------------------------- | ---------------------------------------------- |
| `send(content, channel)` | Send with default options |
| `sendWithOption(content, channel, options)` | Send with custom options |
| `addInsertedListener` | Observe local insertion; one global callback |
| `addSendStatusListener` | Observe the server send result |
| `addNewMsgListener` | Observe new messages |
| `addRefreshListener` | Observe message-object changes |
| `getOrSyncHistoryMessages` | Read local history and synchronize when needed |
| `registerMsgContent` | Register a custom payload |
Send states are `WKSendMsgResult.loading`, `success`, and `fail`, plus relationship restrictions `noRelation`, `blackList`, and `notOnWhiteList`.
## `ConversationManager` [#conversationmanager]
| API | Purpose |
| -------------------- | ------------------------------------- |
| `all()` | Read all local conversations |
| `getWithChannel` | Read one channel's conversation |
| `updateRedDot` | Update an unread count |
| `clearAllRedDot` | Clear all unread counts |
| `delete` | Remove a local conversation-list item |
| `addRefreshListener` | Observe conversation changes |
| `addDeletedListener` | Observe conversation deletion |
## Channel and member managers [#channel-and-member-managers]
| API | Purpose |
| -------------------------------------------- | --------------------------------- |
| `channelManager().getChannel` | Read local channel metadata |
| `channelManager().fetchChannelInfo` | Request a metadata refresh |
| `channelManager().addRefreshListener` | Observe metadata changes |
| `channelMemberManager().sync` | Incrementally synchronize members |
| `channelMemberManager().getMember` | Read one member |
| `channelMemberManager().getWithPageOrSearch` | Page or search members |
## Common models [#common-models]
| Type | Meaning |
| ----------------------------------------------------- | ------------------------------------------------------------- |
| `WKChannel` | `channelId` and `channelType` together identify a chat target |
| `WKMsg` | Message identity, payload, sequence, and send state |
| `WKMessageContent` | Base class for message payloads |
| `WKTextContent` / `WKImageContent` | Built-in text and image payloads |
| `WKConversation` | A conversation-list item and unread count |
| `WKSyncConversation` / `WKSyncChannelMsg` | Sync results returned from your API to the SDK |
| `WKSendOptions` / `ChannelMsgOptions` / `SyncOptions` | Send, history-query, and remote-sync options |
Common channel types are `WKChannelType.personal` and `WKChannelType.group`.
# HarmonyOS channels and metadata (/en/sdk/harmonyos/channels)
A channel answers “who should receive this message?” Always use `channelId` together with `channelType`. A personal channel ID is normally the other user's UID; a group channel ID is the group ID.
```typescript
const personal = new WKChannel('bob', WKChannelType.personal)
const group = new WKChannel('team-1', WKChannelType.group)
```
Constructing `WKChannel` does not create a user or group, and it does not bypass server authorization.
## Supply user and group metadata [#supply-user-and-group-metadata]
Install the metadata provider during application initialization:
```typescript
WKIM.shared.config.provider.channelInfoCallback = async (
channelId: string,
channelType: number
): Promise => {
const profile = await channelApi.get(channelId, channelType)
if (profile === undefined) return undefined
const channel = new WKChannel(channelId, channelType)
channel.channelName = profile.name
channel.channelRemark = profile.remark ?? ''
channel.avatar = profile.avatar ?? ''
return channel
}
```
Read the local cache first, then request metadata when it is absent or stale:
```typescript
const manager = WKIM.shared.channelManager()
const local = manager.getChannel('team-1', WKChannelType.group)
if (local === undefined) {
manager.fetchChannelInfo('team-1', WKChannelType.group)
}
```
Observe refreshes:
```typescript
const refreshListener = (channel: WKChannel) => {
updateTitleAndAvatar(channel)
}
manager.addRefreshListener(refreshListener)
// When the screen is disposed
manager.removeRefreshListener(refreshListener)
```
## Group members [#group-members]
`channelMemberManager()` owns member metadata. Its incremental provider receives the current maximum version:
```typescript
WKIM.shared.config.provider.channelMemberCallback = async (
channelId: string,
channelType: number,
maxVersion: number
): Promise => {
return memberApi.sync(channelId, channelType, maxVersion)
}
WKIM.shared.channelMemberManager().sync(
'team-1',
WKChannelType.group
)
```
Read one member:
```typescript
const member = WKIM.shared.channelMemberManager().getMember(
'team-1',
WKChannelType.group,
'alice'
)
```
Do not return every member of a large group in one response. Use `channelMemberWithPageCallback` with `getWithPageOrSearch` for server-side pagination or search, and keep page sizes bounded.
# HarmonyOS connection (/en/sdk/harmonyos/connection)
The HarmonyOS SDK manages its persistent connection through `WKIM.shared.connectionManager()`. Initialization configures identity and the local database; `connection()` starts the network connection.
## Fixed address [#fixed-address]
When login returns the gateway directly, pass it to `init`:
```typescript
await WKIM.shared.init(
bootstrap.uid,
bootstrap.token,
`${bootstrap.host}:${bootstrap.port}`,
appContext
)
```
Address parsing splits on a colon. Supply a plain DNS or IPv4 `host:port`, not a URI or IPv6 literal.
## Dynamic address [#dynamic-address]
To route before each connection, omit `address` and set the provider instead:
```typescript
await WKIM.shared.init(uid, token, undefined, appContext)
WKIM.shared.config.provider.connectAddrCallback = async (): Promise => {
const route = await gatewayApi.getRoute()
return `${route.host}:${route.port}`
}
```
Throw when routing fails so the SDK enters `fail`; do not return an empty string.
## Connection states [#connection-states]
```typescript
const connectionListener = (status: number, reasonCode?: number) => {
if (status === WKConnectStatus.connecting) {
showConnecting()
} else if (status === WKConnectStatus.success) {
showRestoringConversations()
} else if (status === WKConnectStatus.syncing) {
showRestoringConversations()
} else if (status === WKConnectStatus.syncCompleted) {
enableChat()
} else if (status === WKConnectStatus.noNetwork) {
showOffline()
} else if (status === WKConnectStatus.kicked) {
requireLoginAgain()
} else if (status === WKConnectStatus.fail) {
showConnectionError(reasonCode)
}
}
WKIM.shared.connectionManager().addConnectStatusListener(connectionListener)
WKIM.shared.connectionManager().connection()
```
The normal order is `connecting → success → syncing → syncCompleted`. `success` means the connection and identity check succeeded. `syncCompleted` means the recent-conversation step has finished.
Always set `syncConversationCallback` before connecting. Without it, the state may still reach `syncCompleted`, but offline conversation data will not be restored.
## Disconnect and log out [#disconnect-and-log-out]
```typescript
// Disconnect temporarily while retaining identity and the local database
WKIM.shared.connectionManager().disConnection(false)
// Log out, clear identity, and close the local database
WKIM.shared.connectionManager().disConnection(true)
```
Pass the same function when removing a listener:
```typescript
WKIM.shared.connectionManager().removeConnectStatusListener(connectionListener)
```
`WKIM.shared` and its managers are process singletons. Keep initialization, providers, and global listeners in one application-level IM service; screens should subscribe to that service.
# HarmonyOS conversations (/en/sdk/harmonyos/conversations)
A conversation is one item in the chat list. It keeps the destination channel, last message, time, and unread count. It is neither a connection nor a message payload.
## Connect conversation sync first [#connect-conversation-sync-first]
After connecting, the SDK invokes `syncConversationCallback` to restore conversations changed on other devices or while this client was offline:
```typescript
WKIM.shared.config.provider.syncConversationCallback = async (
lastMsgSeqs: string,
msgCount: number,
version: number
): Promise => {
const response = await conversationApi.sync({
lastMsgSeqs,
msgCount,
version
})
return toWKSyncConversation(response)
}
```
The result should contain the current UID and conversations with channel identity, last message sequence, last client number, unread count, timestamp, version, and recent messages. Do not return an empty array for an account that already has data.
## Read and observe the chat list [#read-and-observe-the-chat-list]
```typescript
const manager = WKIM.shared.conversationManager()
const initial = manager.all() ?? []
renderConversations(initial)
const refreshListener = (changed: WKConversation[]) => {
mergeConversations(changed)
}
const deletedListener = (channelId: string, channelType: number) => {
removeConversation(channelId, channelType)
}
manager.addRefreshListener(refreshListener)
manager.addDeletedListener(deletedListener)
```
`addRefreshListener` delivers changed items, not necessarily the complete list. Merge by `channelId + channelType`, then sort by `lastMsgTimestamp`.
Read one conversation:
```typescript
const conversation = manager.getWithChannel(
'team-1',
WKChannelType.group
)
```
## Unread counts [#unread-counts]
Clear one conversation when its chat screen opens:
```typescript
manager.updateRedDot('team-1', WKChannelType.group, 0)
```
Calculate the total:
```typescript
const totalUnread = (manager.all() ?? [])
.reduce((total, item) => total + item.unreadCount, 0)
```
`clearAllRedDot()` clears unread counts for every conversation.
## Delete a conversation item [#delete-a-conversation-item]
```typescript
manager.delete('team-1', WKChannelType.group)
```
This hides the item from the local chat list; it does not delete server messages. If your product supports cross-device deletion, call the application API first and then update local state.
Remove listeners when the screen is disposed:
```typescript
manager.removeRefreshListener(refreshListener)
manager.removeDeletedListener(deletedListener)
```
# HarmonyOS SDK (/en/sdk/harmonyos)
`@wukong/wkim` is the full HarmonyOS NEXT SDK. It owns the persistent connection, message transport, local messages, conversations, and channel metadata. Your application owns login, tokens, routing, and application HTTP APIs.
If `Channel`, `Conversation`, or provider is unfamiliar, begin with [Core concepts](/en/sdk/wukongim/concepts).
## What the SDK and your application own [#what-the-sdk-and-your-application-own]
| Capability | SDK | Your application |
| --------------------- | -------------------------------------------------------- | ----------------------------------------------------------- |
| Login identity | Connect with a UID and token | Authenticate with your backend and obtain the UID and token |
| Persistent connection | Connection, heartbeat, reconnect, and protocol handling | Supply a reachable `host:port` |
| Messages | Local persistence, sending, receiving, and state updates | Chat UI, application payloads, and failure handling |
| Conversations | Store the chat list and unread counts | Supply the multi-device conversation API |
| Channels | Cache user and group metadata | Supply user, group, and member APIs |
| Media | Manage the media-message send flow | Upload files and return downloadable URLs |
## Import paths [#import-paths]
The package root currently exports only `WKIM`:
```typescript
import { WKIM } from '@wukong/wkim'
```
`WKChannel`, state constants, and message payloads require imports from `@wukong/wkim/src/main/ets/...`. The following guides include the complete paths. Recompile those imports after an SDK upgrade to catch package-layout changes.
The recommended path is Quickstart → Connection → Messages → Conversations → Channels → Advanced. Open the [API reference](/en/sdk/harmonyos/api-reference) when you need a method lookup.
# HarmonyOS messages (/en/sdk/harmonyos/messages)
A message combines a `WKMessageContent` payload with a destination `WKChannel`. `MessageManager` owns local persistence, sending, receiving, and history queries.
## Send personal or group text [#send-personal-or-group-text]
```typescript
const manager = WKIM.shared.messageManager()
manager.send(
new WKTextContent('Hello'),
new WKChannel('bob', WKChannelType.personal)
)
manager.send(
new WKTextContent('Hello, everyone'),
new WKChannel('team-1', WKChannelType.group)
)
```
Use `sendWithOption` for settings such as expiration:
```typescript
const options = new WKSendOptions()
options.expire = 300
manager.sendWithOption(
new WKTextContent('This message expires in five minutes'),
new WKChannel('bob', WKChannelType.personal),
options
)
```
Both send methods return `void`; awaiting them cannot determine success.
## Distinguish three events [#distinguish-three-events]
```typescript
// One global slot with no removal method. Install it once in an application service.
manager.addInsertedListener((message: WKMsg) => {
addPendingMessage(message.clientSeq, message)
})
const sendStatusListener = (
clientSeq: number,
messageId: string,
messageSeq: number,
reasonCode: number
) => {
if (reasonCode === WKSendMsgResult.success) {
markSent(clientSeq, messageId, messageSeq)
} else {
markFailed(clientSeq, reasonCode)
}
}
const newMessagesListener = (messages: WKMsg[]) => {
appendIncoming(messages)
}
manager.addSendStatusListener(sendStatusListener)
manager.addNewMsgListener(newMessagesListener)
```
| Event | Meaning |
| ----------------------- | -------------------------------------------------------------------- |
| `addInsertedListener` | The message entered the local database and can appear in the chat UI |
| `addSendStatusListener` | The server returned a result for this send |
| `addNewMsgListener` | This client received new messages |
`WKSendMsgResult.loading` means sending, `success` means the server accepted the message, and `fail` means failure. `noRelation`, `blackList`, and `notOnWhiteList` are relationship restrictions that should produce clear user-facing errors.
Server acceptance does not mean the recipient read the message. Read state requires a separate product integration.
## Read local and remote history [#read-local-and-remote-history]
```typescript
const channel = new WKChannel('bob', WKChannelType.personal)
const options = new ChannelMsgOptions(
() => showHistoryLoading(),
(messages: WKMsg[]) => showMessages(messages)
)
options.limit = 30
options.pullMode = 0
options.oldestOrderSeq = 0
manager.getOrSyncHistoryMessages(channel, options)
```
The SDK checks the local database first and calls `syncMessageCallback` when local data is insufficient. See [Media and history](/en/sdk/harmonyos/advanced/media-and-history) for provider setup.
## Remove listeners [#remove-listeners]
```typescript
manager.removeSendStatusListener(sendStatusListener)
manager.removeNewMsgListener(newMessagesListener)
```
Removal requires the same function object used during registration. `addInsertedListener` has no removal method, and a later registration replaces the previous one, so do not register it from individual screens.
# HarmonyOS quickstart (/en/sdk/harmonyos/quickstart)
This page has one goal: exchange one text message between two online HarmonyOS clients.
## Prerequisites [#prerequisites]
* Your application backend returns a `uid`, `token`, and `host:port` for both `alice` and `bob`.
* You have two independent application processes or devices. `WKIM.shared` is a process singleton.
* The project targets HarmonyOS NEXT and declares `ohos.permission.INTERNET` and `ohos.permission.GET_NETWORK_INFO`.
## 1. Install [#1-install]
Pin the package in your project directory:
```bash
ohpm install @wukong/wkim@1.1.7
```
## 2. Import and initialize [#2-import-and-initialize]
```typescript
import { Context } from '@kit.AbilityKit'
import { WKIM } from '@wukong/wkim'
import {
ConnectionInfo,
WKChannel,
WKChannelType,
WKConnectStatus,
WKMsg,
WKSendMsgResult,
WKSyncConversation
} from '@wukong/wkim/src/main/ets/entity/Bean'
import { WKTextContent } from '@wukong/wkim/src/main/ets/model/WKTextContent'
const im = WKIM.shared
await im.init(bootstrap.uid, bootstrap.token, bootstrap.address, appContext as Context)
```
`bootstrap.address` should look like `im.example.com:5100`. Do not include `tcp://`, `http://`, or `https://`.
## 3. Supply conversation sync [#3-supply-conversation-sync]
Before the connection becomes ready, the SDK asks your backend for recent conversations:
```typescript
im.config.provider.syncConversationCallback = async (
lastMsgSeqs: string,
msgCount: number,
version: number
): Promise => {
return conversationApi.sync(lastMsgSeqs, msgCount, version)
}
```
For a brand-new development account only, you may temporarily return an empty result:
```typescript
const result = new WKSyncConversation()
result.uid = bootstrap.uid
result.conversations = []
return result
```
An account with existing chat data must receive its real conversations and recent messages.
## 4. Observe connection and messages [#4-observe-connection-and-messages]
Keep listeners as properties or application-level constants. Removal requires the same function object:
```typescript
const connectionListener = (
status: number,
reasonCode?: number,
info?: ConnectionInfo
) => {
if (status === WKConnectStatus.syncCompleted) {
console.info(`WuKongIM ready on node ${info?.nodeId ?? '-'}`)
} else if (status === WKConnectStatus.fail) {
console.error(`connect failed: ${reasonCode ?? '-'}`)
} else if (status === WKConnectStatus.kicked) {
requireLoginAgain()
}
}
const sendStatusListener = (
clientSeq: number,
messageId: string,
messageSeq: number,
reasonCode: number
) => {
if (reasonCode === WKSendMsgResult.success) {
markMessageSent(clientSeq, messageId, messageSeq)
} else {
markMessageFailed(clientSeq, reasonCode)
}
}
const newMessagesListener = (messages: WKMsg[]) => {
messages.forEach((message) => {
if (message.messageContent instanceof WKTextContent) {
console.info(`${message.fromUID}: ${message.messageContent.content}`)
}
})
}
im.connectionManager().addConnectStatusListener(connectionListener)
im.messageManager().addSendStatusListener(sendStatusListener)
im.messageManager().addNewMsgListener(newMessagesListener)
```
`success` means the connection protocol succeeded. `syncing` and `syncCompleted` follow it. Enable sending only after `syncCompleted`.
## 5. Connect and send text [#5-connect-and-send-text]
```typescript
im.connectionManager().connection()
```
After Alice reaches `syncCompleted`, send to Bob:
```typescript
im.messageManager().send(
new WKTextContent('Hello, Bob'),
new WKChannel('bob', WKChannelType.personal)
)
```
`send()` does not return a Promise. The `reasonCode` delivered to `sendStatusListener` tells you whether the server accepted the message.
## Expected result [#expected-result]
1. Alice and Bob both reach `syncCompleted`.
2. Alice receives `WKSendMsgResult.success` in the send callback.
3. Bob's new-message listener prints “Hello, Bob”.
Remove listeners when the screen or application service ends:
```typescript
im.connectionManager().removeConnectStatusListener(connectionListener)
im.messageManager().removeSendStatusListener(sendStatusListener)
im.messageManager().removeNewMsgListener(newMessagesListener)
```
Continue with [Connection](/en/sdk/harmonyos/connection) and [Messages](/en/sdk/harmonyos/messages).
# iOS API Reference (/en/sdk/ios/api-reference)
This is a lookup page for common entry points, not a replacement for the task guides. Access every manager from `[WKSDK shared]`.
## SDK and configuration [#sdk-and-configuration]
| API | Purpose |
| ------------------------- | ------------------------------------------------------- |
| `[WKSDK shared]` | SDK singleton |
| `options` | Endpoint, identity, debug, and synchronization settings |
| `registerMessageContent:` | Register custom message content |
| `sdkVersion` | Version running in the app |
## `WKConnectionManager` [#wkconnectionmanager]
| API | Purpose |
| ---------------------------------- | ----------------------------------------------------- |
| `connect` | Start a connection attempt |
| `disconnect:` | Disconnect; pass `YES` to stop automatic reconnection |
| `logout` | Sign out and clear login information |
| `addDelegate:` / `removeDelegate:` | Manage connection observers |
| `getConnectAddr` | Supply a dynamic `host:port` |
| `connectStatus` | Current `WKConnectStatus` |
Callbacks: `onConnectStatus:reasonCode:` and `onKick:reason:`.
## `WKChatManager` [#wkchatmanager]
| API | Purpose |
| ---------------------------------------- | ----------------------------------- |
| `sendMessage:channel:` | Send and store a message |
| `resendMessage:` | Retry a failed message |
| `pullLastMessages:limit:complete:` | Read the latest messages |
| `pullDown:startOrderSeq:limit:complete:` | Read older messages |
| `pullUp:startOrderSeq:limit:complete:` | Read newer messages |
| `clearMessages:` | Clear local messages in one channel |
| `syncChannelMessageProvider` | Connect remote channel history |
Callbacks: `onRecvMessages:left:` and `onMessageUpdate:left:`.
## `WKConversationManager` [#wkconversationmanager]
| API | Purpose |
| ---------------------------------------- | -------------------------------------- |
| `getConversationList` | Read recent conversations |
| `getConversation:` | Read the conversation for one channel |
| `clearConversationUnreadCount:` | Clear unread count |
| `getAllConversationUnreadCount` | Total all unread counts |
| `deleteConversation:` | Delete a conversation row |
| `setSyncConversationProviderAndAck:ack:` | Connect multi-device conversation sync |
Callbacks: `onConversationUpdate:` and `onConversationUnreadCountUpdate:unreadCount:`.
## `WKChannelManager` [#wkchannelmanager]
| API | Purpose |
| ------------------------------- | -------------------------------------- |
| `getChannelInfo:` | Read a local channel profile |
| `fetchChannelInfo:completion:` | Request a profile refresh |
| `addOrUpdateChannelInfo:` | Store a channel profile |
| `getMembersWithChannel:limit:` | Read local group members |
| `addOrUpdateMembers:` | Store group members |
| `updateChannelSetting:setting:` | Change mute, pin, and related settings |
Callback: `channelInfoUpdate:oldChannelInfo:`. The remote profile entry point is `[WKSDK shared].channelInfoUpdate`.
## Common models and states [#common-models-and-states]
| Type | Meaning |
| ------------------ | ---------------------------------------------- |
| `WKConnectInfo` | UID, token, name, and avatar |
| `WKChannel` | `channelId` plus `channelType` |
| `WKMessage` | Identity, sender, content, sequence, and state |
| `WKMessageContent` | Base class for message content |
| `WKConversation` | Chat-list row and unread count |
| `WKChannelInfo` | Name, avatar, and channel settings |
| `WKChannelMember` | Group member profile |
Connection states: `WKNoConnect`, `WKConnecting`, `WKPullingOffline`, `WKConnected`, and `WKDisconnected`.
Message states: `WK_MESSAGE_WAITSEND`, `WK_MESSAGE_UPLOADING`, `WK_MESSAGE_SUCCESS`, and `WK_MESSAGE_FAIL`.
# iOS Channels (/en/sdk/ios/channels)
A `WKChannel` answers “who should receive this message?” A direct channel ID is the other user's UID; a group channel ID is your product's group ID. `WKChannelInfo` stores display data such as name, avatar, and settings.
## Create channels [#create-channels]
```objc
WKChannel *person = [WKChannel personWithChannelID:@"bob"];
WKChannel *group = [WKChannel groupWithChannelID:@"team-1"];
```
The same `channelId` under different `channelType` values is not the same chat. Use the complete `WKChannel` identity in caches and lists.
## Read local data and refresh when needed [#read-local-data-and-refresh-when-needed]
```objc
WKChannelInfo *cached =
[[WKSDK shared].channelManager getChannelInfo:person];
[[WKSDK shared].channelManager fetchChannelInfo:person
completion:^(WKChannelInfo *info) {
self.titleLabel.text = info.displayName;
}];
```
`getChannelInfo:` only reads local data and can return `nil`. `fetchChannelInfo:completion:` requests a profile through the data source supplied by your app.
## Connect your user and group profile API [#connect-your-user-and-group-profile-api]
```objc
[WKSDK shared].channelInfoUpdate = ^WKTaskOperator *(WKChannel *channel,
WKChannelInfoCallback callback) {
return [profileAPI fetchChannel:channel completion:^(WKChannelInfo *info,
NSError *error) {
if (info != nil) {
[[WKSDK shared].channelManager addOrUpdateChannelInfo:info];
}
callback(error, NO);
}];
};
```
Choose the user or group endpoint from `channel.channelType`, and always invoke `callback`. The returned task supports cancellation; return `nil` if your network layer has no cancellable task.
## Observe profile changes [#observe-profile-changes]
```objc
[[WKSDK shared].channelManager addDelegate:self];
- (void)channelInfoUpdate:(WKChannelInfo *)channelInfo
oldChannelInfo:(WKChannelInfo *)oldChannelInfo {
[self updateTitle:channelInfo.displayName avatar:channelInfo.logo];
}
```
Group members come from the same manager:
```objc
NSArray *members =
[[WKSDK shared].channelManager getMembersWithChannel:group limit:50];
```
The SDK manages local member data only. Your app fetches, paginates, and updates members from its server, then stores them through `addOrUpdateMembers:`.
## Common problems [#common-problems]
* Conversation has only an ID: configure `channelInfoUpdate`, then call `fetchChannelInfo:`.
* Avatar never changes: ensure the new `WKChannelInfo.version` is newer, or deliberately use `addOrUpdateChannelInfo:` without version comparison.
* Large group screen stalls: paginate members instead of loading the full group into the UI.
# iOS Connection (/en/sdk/ios/connection)
`WKConnectionManager` owns the connection. Your app provides user identity and an endpoint; the SDK handles the socket, heartbeat, and automatic reconnection.
## Configure identity and a fixed endpoint [#configure-identity-and-a-fixed-endpoint]
```objc
WKOptions *options = [WKOptions new];
options.host = @"im.example.com";
options.port = 5100;
options.connectInfo = [WKConnectInfo initWithUID:@"alice"
token:token
name:@""
avatar:@""];
[WKSDK shared].options = options;
```
Use `connectInfoCallback` to supply the latest token before each connection:
```objc
[WKSDK shared].options.connectInfoCallback = ^WKConnectInfo *{
return [WKConnectInfo initWithUID:currentUID
token:currentToken
name:@""
avatar:@""];
};
```
Never print the token in logs.
## Use a dynamic endpoint [#use-a-dynamic-endpoint]
If your application server assigns a gateway, return `host:port` from `getConnectAddr`:
```objc
[WKSDK shared].connectionManager.getConnectAddr = ^(void (^complete)(NSString *addr)) {
[gatewayAPI fetchAddress:^(NSString *host, uint16_t port) {
complete([NSString stringWithFormat:@"%@:%hu", host, port]);
}];
};
```
Always invoke `complete`; otherwise that connection attempt keeps waiting for an address.
## Wait for the state callback before sending [#wait-for-the-state-callback-before-sending]
```objc
[[WKSDK shared].connectionManager addDelegate:self];
[[WKSDK shared].connectionManager connect];
- (void)onConnectStatus:(WKConnectStatus)status reasonCode:(WKReason)reasonCode {
switch (status) {
case WKConnecting:
self.sendButton.enabled = NO;
break;
case WKConnected:
self.sendButton.enabled = YES;
break;
case WKDisconnected:
self.sendButton.enabled = NO;
break;
default:
break;
}
}
- (void)onKick:(uint8_t)reasonCode reason:(NSString *)reason {
// Clear the app session and explain that the account signed in elsewhere.
}
```
`WKPullingOffline` means the connection is established while the SDK restores offline messages. Waiting for `WKConnected` before enabling send is the simplest UI rule.
## Disconnect or log out [#disconnect-or-log-out]
```objc
// Stop IM use here and disable automatic reconnection.
[[WKSDK shared].connectionManager disconnect:YES];
// Sign out: disconnect and clear the SDK login information.
[[WKSDK shared].connectionManager logout];
```
Remove the delegate before its owner is destroyed:
```objc
[[WKSDK shared].connectionManager removeDelegate:self];
```
## Common problems [#common-problems]
* Stuck while connecting: verify that the address callback completes, the port is reachable, and the token belongs to this UID.
* Reconnects after disconnect: `disconnect:NO` permits reconnection; pass `YES` to stop it.
* Old data after switching accounts: use `logout`, then replace `WKOptions` before connecting the new account.
Continue with [Messages](/en/sdk/ios/messages).
# iOS Conversations (/en/sdk/ios/conversations)
A `WKConversation` is one row in the chat list. It contains the destination channel, last message, last-message time, and unread count.
## Read the chat list [#read-the-chat-list]
```objc
NSArray *items =
[[WKSDK shared].conversationManager getConversationList];
for (WKConversation *item in items) {
NSLog(@"channel=%@ unread=%ld",
item.channel.channelId,
(long)item.unreadCount);
}
```
Use `lastMessage` for the preview and `lastMsgTimestamp` for ordering or display. `channelInfo` can be unavailable until its profile loads; see [Channels](/en/sdk/ios/channels) for names and avatars.
## Observe list changes [#observe-list-changes]
```objc
[[WKSDK shared].conversationManager addDelegate:self];
- (void)onConversationUpdate:(NSArray *)conversations {
// The array contains changed items; merge them by channel.
[self mergeConversations:conversations];
}
- (void)onConversationUnreadCountUpdate:(WKChannel *)channel
unreadCount:(NSInteger)unreadCount {
[self updateBadgeForChannel:channel count:unreadCount];
}
```
For a small list, re-reading `getConversationList` after each change is also reasonable and simpler.
## Clear and total unread counts [#clear-and-total-unread-counts]
After the user opens a chat and sees its messages, clear the local unread count:
```objc
WKChannel *channel = [WKChannel personWithChannelID:@"bob"];
[[WKSDK shared].conversationManager clearConversationUnreadCount:channel];
NSInteger total =
[[WKSDK shared].conversationManager getAllConversationUnreadCount];
```
Deleting a conversation row does not delete every local message for that channel:
```objc
[[WKSDK shared].conversationManager deleteConversation:channel];
```
## Connect server-side conversation sync [#connect-server-side-conversation-sync]
To restore recent conversations across devices, connect your application API through `setSyncConversationProviderAndAck:ack:`. The provider receives a version and each channel's last message sequence, then returns a `WKSyncConversationWrapModel`. The ack submits the processed version to your server.
Keep UI work out of the provider; it is a data synchronization boundary. The conversation delegate reports changes after synchronization.
Call `removeDelegate:` when the owner leaves the screen to prevent duplicate updates.
# iOS (/en/sdk/ios)
`WuKongIMSDK` is the full WuKongIM SDK for iOS. It manages the persistent connection, message delivery, local message database, conversation list, and channel profiles. Use it when your app owns the chat UI.
These guides target `WuKongIMSDK 1.1.1` and use Objective-C throughout.
## Before you start [#before-you-start]
Prepare:
* a reachable WuKongIM service;
* two test users, such as `alice` and `bob`;
* a `uid`, `token`, connection `host`, and `port` returned by your application server;
* an iOS project that uses CocoaPods.
Your application server should create the token and return it to an authenticated user. Never put a WuKongIM admin secret in the app or let the app call an admin API to create tokens.
## Recommended order [#recommended-order]
Read [Core Concepts](/en/sdk/wukongim/concepts) first if “channel” or “conversation” is unfamiliar. Open the [API Reference](/en/sdk/ios/api-reference) when you only need a method name.
# iOS Messages (/en/sdk/ios/messages)
A message combines `WKMessageContent` with a destination `WKChannel`. `WKChatManager` handles sending, receiving, local storage, and queries.
## Send to a direct or group chat [#send-to-a-direct-or-group-chat]
```objc
WKTextContent *content = [[WKTextContent alloc] initWithContent:@"Hello"];
WKChannel *person = [WKChannel personWithChannelID:@"bob"];
WKMessage *directMessage = [[WKSDK shared].chatManager sendMessage:content
channel:person];
WKChannel *group = [WKChannel groupWithChannelID:@"team-1"];
WKMessage *groupMessage = [[WKSDK shared].chatManager sendMessage:content
channel:group];
```
The returned `WKMessage` is a local message; it does not mean the server has finished processing it. Use `clientMsgNo` to match it with later state changes.
## Observe incoming messages and send states [#observe-incoming-messages-and-send-states]
```objc
[[WKSDK shared].chatManager addDelegate:self];
- (void)onRecvMessages:(WKMessage *)message left:(NSInteger)left {
// Inspect message.content before rendering text, media, or custom content.
[self appendMessage:message];
}
- (void)onMessageUpdate:(WKMessage *)message left:(NSInteger)left {
[self updateMessage:message.clientMsgNo status:message.status];
}
```
Common states:
| State | Meaning | Suggested UI |
| ---------------------- | ---------------------------------- | ------------- |
| `WK_MESSAGE_WAITSEND` | Stored locally and waiting to send | Sending |
| `WK_MESSAGE_UPLOADING` | Uploading media | Show progress |
| `WK_MESSAGE_SUCCESS` | Accepted by the server | Sent |
| `WK_MESSAGE_FAIL` | This send attempt failed | Retry action |
Retry a failed message with:
```objc
[[WKSDK shared].chatManager resendMessage:failedMessage];
```
## Read messages for one chat [#read-messages-for-one-chat]
Load the latest 30 messages when opening a chat:
```objc
WKChannel *channel = [WKChannel personWithChannelID:@"bob"];
[[WKSDK shared].chatManager pullLastMessages:channel
limit:30
complete:^(NSArray *messages, NSError *error) {
if (error == nil) {
[self showMessages:messages];
}
}];
```
Use `pullDown:startOrderSeq:limit:complete:` for older pages and `pullUp:startOrderSeq:limit:complete:` toward newer messages.
## Remove the delegate [#remove-the-delegate]
```objc
[[WKSDK shared].chatManager removeDelegate:self];
```
Do not repeatedly add the same delegate from multiple screens without removing it, or one message can produce duplicate UI updates. See [Media & History](/en/sdk/ios/advanced/media-and-history) for remote history and media tasks.
# iOS Quickstart (/en/sdk/ios/quickstart)
This page has one goal: exchange a text message between two online iOS clients. Add conversations, channel profiles, and offline synchronization after this works.
## Prerequisites [#prerequisites]
* Your application server returns a `uid`, `token`, `host`, and `port` for both `alice` and `bob`.
* You have two separate app processes or devices. `WKSDK` is a singleton and cannot represent two signed-in users in one process.
* CocoaPods works in your Objective-C project.
## 1. Install [#1-install]
Pin the version in your `Podfile`:
```ruby
pod 'WuKongIMSDK', '1.1.1'
```
Then run:
```bash
pod install
```
Open the generated `.xcworkspace` from now on.
## 2. Configure and connect [#2-configure-and-connect]
Create one object that owns the connection and message delegates:
```objc
#import
@interface IMClient : NSObject
- (void)startWithUID:(NSString *)uid
token:(NSString *)token
host:(NSString *)host
port:(uint16_t)port;
- (void)sendText:(NSString *)text toUID:(NSString *)uid;
@end
@implementation IMClient
- (void)startWithUID:(NSString *)uid
token:(NSString *)token
host:(NSString *)host
port:(uint16_t)port {
WKOptions *options = [WKOptions new];
options.host = host;
options.port = port;
options.isDebug = NO;
options.connectInfo = [WKConnectInfo initWithUID:uid
token:token
name:@""
avatar:@""];
[WKSDK shared].options = options;
[[WKSDK shared].connectionManager addDelegate:self];
[[WKSDK shared].chatManager addDelegate:self];
[[WKSDK shared].connectionManager connect];
}
- (void)onConnectStatus:(WKConnectStatus)status reasonCode:(WKReason)reasonCode {
if (status == WKConnected) {
NSLog(@"WuKongIM connected");
} else if (status == WKDisconnected) {
NSLog(@"WuKongIM disconnected, reason: %ld", (long)reasonCode);
}
}
```
`connect` only starts the attempt. The client is ready to send after the delegate reports `WKConnected`.
## 3. Send a text message [#3-send-a-text-message]
After Alice connects, send to Bob's UID:
```objc
- (void)sendText:(NSString *)text toUID:(NSString *)uid {
WKTextContent *content = [[WKTextContent alloc] initWithContent:text];
WKChannel *channel = [WKChannel personWithChannelID:uid];
[[WKSDK shared].chatManager sendMessage:content channel:channel];
}
```
Call it with:
```objc
[client sendText:@"Hello, Bob" toUID:@"bob"];
```
## 4. Observe the result [#4-observe-the-result]
Use the same delegate for local send-state changes and incoming messages:
```objc
- (void)onMessageUpdate:(WKMessage *)message left:(NSInteger)left {
if (message.status == WK_MESSAGE_SUCCESS) {
NSLog(@"message sent: %@", message.clientMsgNo);
} else if (message.status == WK_MESSAGE_FAIL) {
NSLog(@"message failed: %@", message.clientMsgNo);
}
}
- (void)onRecvMessages:(WKMessage *)message left:(NSInteger)left {
if ([message.content isKindOfClass:[WKTextContent class]]) {
WKTextContent *text = (WKTextContent *)message.content;
NSLog(@"%@ says: %@", message.fromUid, text.content);
}
}
@end
```
## Expected result [#expected-result]
1. Alice and Bob both report `WKConnected`.
2. Alice receives `WK_MESSAGE_SUCCESS` in `onMessageUpdate`.
3. Bob prints “Hello, Bob” from `onRecvMessages`.
If connection fails, make sure `host` has no `http://` or `https://` prefix and `port` is the client persistent-connection port. If sending produces no result, verify that Bob is online, the UID is correct, and sending happens after `WKConnected`.
Continue with [Connection](/en/sdk/ios/connection) and [Messages](/en/sdk/ios/messages).
# JavaScript / Web API Reference (/en/sdk/javascript/api-reference)
This is a lookup page for common entry points, not a replacement for the task guides. The root singleton is `WKSDK.shared()`.
## `WKSDK` and configuration [#wksdk-and-configuration]
| API | Purpose |
| ------------------------------- | ------------------------------------------ |
| `WKSDK.shared()` | SDK singleton for the current page context |
| `config.uid` / `config.token` | Current user identity |
| `config.addr` | WebSocket endpoint |
| `config.deviceFlag` | `0` App, `1` Web, `2` PC |
| `config.provider` | Application data sources |
| `connect()` / `disconnect()` | Connect and stop connecting |
| `register(type, factory)` | Register custom message content |
| `newChannel` / `newMessageText` | Convenience model constructors |
## `ConnectManager` [#connectmanager]
| API | Purpose |
| ----------------------------- | ------------------------ |
| `addConnectStatusListener` | Observe connection state |
| `removeConnectStatusListener` | Remove a state observer |
| `addConnectDelayListener` | Observe heartbeat delay |
| `removeConnectDelayListener` | Remove a delay observer |
| `status` | Current `ConnectStatus` |
States: `Disconnect`, `Connected`, `Connecting`, `ConnectFail`, and `ConnectKick`.
## `ChatManager` [#chatmanager]
| API | Purpose |
| ---------------------------------------------------------- | ------------------------------------------ |
| `send(content, channel, setting?)` | Send and return a local `Message` |
| `sendWithOptions(content, channel, options)` | Send with advanced options |
| `addMessageListener` / `removeMessageListener` | Observe all incoming and outgoing messages |
| `addMessageStatusListener` / `removeMessageStatusListener` | Observe server send results |
| `syncMessages(channel, options)` | Synchronize through the provider |
| `syncMessageExtras` | Synchronize message extra data |
## `ConversationManager` [#conversationmanager]
| API | Purpose |
| -------------------------------------------------------- | ------------------------------------------------------- |
| `sync(filter?)` | Synchronize conversations from the application provider |
| `conversations` | Current in-memory conversation list |
| `findConversation(channel)` | Find one conversation |
| `createEmptyConversation(channel)` | Create an empty conversation |
| `removeConversation(channel)` | Remove an in-memory row |
| `getAllUnreadCount()` | Total all unread counts |
| `addConversationListener` / `removeConversationListener` | Observe list changes |
## `ChannelManager` [#channelmanager]
| API | Purpose |
| -------------------------------- | ------------------------------------ |
| `fetchChannelInfo(channel)` | Fetch a profile through the provider |
| `getChannelInfo(channel)` | Read a cached profile |
| `addListener` / `removeListener` | Observe profile changes |
| `syncSubscribes(channel)` | Synchronize group subscribers |
| `getSubscribes(channel)` | Read cached subscribers |
## Common providers [#common-providers]
| Configuration | Result |
| --------------------------- | ------------------------------------------------ |
| `connectAddrCallback` | Supplies a WebSocket endpoint through a callback |
| `channelInfoCallback` | `Promise` |
| `syncSubscribersCallback` | `Promise` |
| `syncConversationsCallback` | `Promise` |
| `syncMessagesCallback` | `Promise` |
| `messageUploadTaskCallback` | `MessageTask` |
## Common models [#common-models]
`Channel`, `ChannelInfo`, `Subscriber`, `Message`, `MessageContent`, `MessageText`, `MessageImage`, `Conversation`, `SyncOptions`, and `SendOptions`.
Common channel types: `ChannelTypePerson` and `ChannelTypeGroup`. Local message states: `MessageStatus.Wait`, `MessageStatus.Normal`, and `MessageStatus.Fail`.
# JavaScript / Web Channels (/en/sdk/javascript/channels)
A `Channel` answers “who should receive this message?” A direct channel ID is the other user's UID; a group channel ID is your product's group ID. `ChannelInfo` stores the title and avatar needed by the UI.
## Create channels [#create-channels]
```typescript
const person = new Channel('bob', ChannelTypePerson)
const group = new Channel('team-1', ChannelTypeGroup)
```
`channelID + channelType` is the complete identity. `channel.getChannelKey()` is useful as a frontend cache key.
## Connect your user and group profile API [#connect-your-user-and-group-profile-api]
```typescript
sdk.config.provider.channelInfoCallback = async (channel) => {
const profile = await profileApi.get(
channel.channelID,
channel.channelType,
)
const info = new ChannelInfo()
info.channel = channel
info.title = profile.displayName
info.logo = profile.avatarUrl
info.mute = profile.mute
info.top = profile.top
info.orgData = profile
return info
}
```
Request and read the profile:
```typescript
await sdk.channelManager.fetchChannelInfo(person)
const info = sdk.channelManager.getChannelInfo(person)
```
## Observe profile changes [#observe-profile-changes]
```typescript
const onChannelInfo = (info: ChannelInfo) => {
updateChatHeader(info.title, info.logo)
}
sdk.channelManager.addListener(onChannelInfo)
```
Call `removeListener(onChannelInfo)` when the page is destroyed.
## Group subscribers [#group-subscribers]
For a group member or subscriber list, implement `syncSubscribersCallback(channel, version)` and call `channelManager.syncSubscribes(channel)`. The provider returns `Subscriber[]`; each item needs a UID, channel, version, and deletion state. Large groups require pagination or incremental sync rather than loading every member into browser memory.
## Common problems [#common-problems]
* Empty title: configure `channelInfoCallback` before calling `fetchChannelInfo`.
* Direct and group profiles get mixed: every cache key must include the channel type.
* Provider errors leave no feedback: catch the Promise around `fetchChannelInfo` in the UI and offer retry.
# JavaScript / Web Connection (/en/sdk/javascript/connection)
`ConnectManager` owns WebSocket, heartbeat, and automatic reconnection. Your app provides the current user's UID, token, and endpoint.
## Configure a fixed endpoint [#configure-a-fixed-endpoint]
```typescript
const sdk = WKSDK.shared()
sdk.config.uid = bootstrap.uid
sdk.config.token = bootstrap.token
sdk.config.addr = bootstrap.websocketUrl
sdk.config.deviceFlag = 1
```
The endpoint must be a complete `ws://` or `wss://` URL. An HTTPS page normally must connect over `wss://`.
## Resolve an endpoint dynamically [#resolve-an-endpoint-dynamically]
To query routing before each connection:
```typescript
sdk.config.provider.connectAddrCallback = (complete) => {
void gatewayApi.getAddress().then(({ websocketUrl }) => {
complete(websocketUrl)
})
}
```
This provider uses a callback; it does not return a Promise to the SDK. Show application errors and schedule backoff when the request fails. Never place the token in the URL or logs.
## Observe connection state [#observe-connection-state]
```typescript
const onStatus = (
status: ConnectStatus,
reasonCode?: number,
info?: ConnectionInfo,
) => {
switch (status) {
case ConnectStatus.Connecting:
showConnecting()
break
case ConnectStatus.Connected:
enableSending(info?.nodeId)
break
case ConnectStatus.Disconnect:
showOffline()
break
case ConnectStatus.ConnectFail:
showConnectionError(reasonCode)
break
case ConnectStatus.ConnectKick:
requireLoginAgain(reasonCode)
break
}
}
sdk.connectManager.addConnectStatusListener(onStatus)
sdk.connect()
```
The SDK reconnects after network failures. It does not retry forever after server rejection or a kick.
## Page and account lifecycle [#page-and-account-lifecycle]
```typescript
sdk.connectManager.removeConnectStatusListener(onStatus)
sdk.disconnect()
```
`disconnect()` stops automatic reconnection. Replace `uid`, `token`, and endpoint before reconnecting. Because the SDK is a global singleton, remove all old listeners and disconnect before initializing another account; do not share one page context between two accounts.
## Common problems [#common-problems]
* Browser reports Mixed Content: an HTTPS page must use `wss://`.
* Duplicate state callbacks: a framework component mounted the listener repeatedly and did not remove the same function reference.
* Works locally but not in production: verify WebSocket Upgrade forwarding, the certificate chain, and CSP `connect-src`.
# JavaScript / Web Conversations (/en/sdk/javascript/conversations)
A `Conversation` is one row in the chat list. The JavaScript SDK keeps this list in memory; your application server restores it after a page reload.
## Connect conversation synchronization [#connect-conversation-synchronization]
```typescript
sdk.config.provider.syncConversationsCallback = async (filter) => {
const rows = await conversationApi.list(filter)
return rows.map(toSDKConversation)
}
const conversations = await sdk.conversationManager.sync()
renderConversationList(conversations)
```
Each mapped `Conversation` needs at least `channel`, `unread`, `timestamp`, and an optional `lastMessage`.
## Observe list changes [#observe-list-changes]
```typescript
const onConversation = (
conversation: Conversation,
action: ConversationAction,
) => {
updateConversationRow(conversation, action)
}
sdk.conversationManager.addConversationListener(onConversation)
```
Live incoming and outgoing messages automatically add or update conversations. `ConversationAction` contains `add`, `update`, and `remove`.
## Open a chat and handle unread count [#open-a-chat-and-handle-unread-count]
```typescript
const channel = new Channel('bob', ChannelTypePerson)
const conversation = sdk.conversationManager.findConversation(channel)
if (conversation) {
sdk.conversationManager.openConversation = conversation
conversation.unread = 0
sdk.conversationManager.notifyConversationListeners(
conversation,
ConversationAction.update,
)
}
```
Once `openConversation` is set, new live messages in that channel do not increment the in-memory unread count. Set it to `undefined` when leaving. If unread counts must match across devices, also update your application API and restore the server value through conversation sync.
Total unread count:
```typescript
const total = sdk.conversationManager.getAllUnreadCount()
```
Removing a row only changes the current in-memory list:
```typescript
sdk.conversationManager.removeConversation(channel)
```
Call `removeConversationListener(onConversation)` when the page is destroyed.
# JavaScript / Web (/en/sdk/javascript)
`wukongimjssdk` is the full WuKongIM JavaScript SDK. It manages WebSocket, message delivery, an in-memory conversation list, and channel profiles for browsers, H5, and separately verified JavaScript runtimes.
These guides target `wukongimjssdk 1.3.5` and use TypeScript throughout.
## Before you start [#before-you-start]
Prepare:
* a WuKongIM WebSocket endpoint reachable from the target runtime;
* two test users, such as `alice` and `bob`;
* a `uid`, `token`, and `ws://` or `wss://` endpoint returned by your application server;
* a frontend project using npm, pnpm, or Yarn.
The browser should receive only the current user's short-lived token and endpoint from your application server. Keep user creation, token issuance, and access control on the server.
## Recommended order [#recommended-order]
The repository contains a runnable two-session example at `docs-site/examples/javascript-web-quickstart`. Read these guides to learn the API first; run the example when you are ready to connect a real service.
Read [Core Concepts](/en/sdk/wukongim/concepts) first if “channel” or “conversation” is unfamiliar. Open the [API Reference](/en/sdk/javascript/api-reference) when you only need a method name.
# JavaScript / Web Messages (/en/sdk/javascript/messages)
A message combines `MessageContent` with a destination `Channel`. `ChatManager` handles sending, live delivery, and calls to the remote synchronization provider.
## Send to a direct or group chat [#send-to-a-direct-or-group-chat]
```typescript
const direct = await sdk.chatManager.send(
new MessageText('Hello'),
new Channel('bob', ChannelTypePerson),
)
const group = await sdk.chatManager.send(
new MessageText('Hello everyone'),
new Channel('team-1', ChannelTypeGroup),
)
```
The result is a local `Message` with initial state `MessageStatus.Wait`. Use `clientSeq` or `clientMsgNo` to correlate the later server result.
## Observe messages and send results [#observe-messages-and-send-results]
```typescript
const onMessage = (message: Message) => {
if (message.send) {
addOutgoingMessage(message)
} else {
addIncomingMessage(message)
}
}
const onStatus = (ack: SendackPacket) => {
updateSendState(ack.clientSeq, ack.reasonCode, ack.messageSeq)
}
sdk.chatManager.addMessageListener(onMessage)
sdk.chatManager.addMessageStatusListener(onStatus)
```
Sending immediately invokes the message listener, so not every `onMessage` is a new peer message. `reasonCode === 1` means the server accepted the send; the peer's listener separately confirms live delivery.
## Synchronize message history [#synchronize-message-history]
The JavaScript SDK has no built-in persistent message database. Your application API supplies offline recovery and pagination:
```typescript
sdk.config.provider.syncMessagesCallback = async (channel, options) => {
const rows = await messageApi.sync({
channelID: channel.channelID,
channelType: channel.channelType,
startMessageSeq: options.startMessageSeq,
endMessageSeq: options.endMessageSeq,
limit: options.limit,
pullMode: options.pullMode,
})
return rows.map(toSDKMessage)
}
```
Request synchronization with:
```typescript
const options = new SyncOptions()
options.limit = 30
options.pullMode = PullMode.Down
const messages = await sdk.chatManager.syncMessages(
new Channel('bob', ChannelTypePerson),
options,
)
```
`toSDKMessage` is your mapping function. It must populate `messageID`, `messageSeq`, `clientMsgNo`, `fromUID`, `channel`, `timestamp`, and correctly decoded `content`.
On page teardown, call `removeMessageListener` and `removeMessageStatusListener` with the original function references.
# JavaScript / Web Quickstart (/en/sdk/javascript/quickstart)
This page has one goal: exchange a text message between two online browser sessions.
**① SENDACK** is the server send result. **② RECEIVED** is the recipient's incoming event. Run this example first, then integrate the code into your app. Click images to enlarge them.
## Run the two-user example first [#run-the-two-user-example-first]
Install Git, Node.js 20.11 or newer, and npm. If you do not yet have an application backend, the example includes a loopback-only Node.js service (BFF) that prepares development identities and discovers connection endpoints.
### Start WuKongIM [#start-wukongim]
Follow [Docker Deployment](/en/server/deployment/docker) or [Start a Single-node Cluster](/en/guide/quick-start/single-node-cluster).
Using a remote server? Set up SSH forwarding first
If WuKongIM runs on a remote Linux server, open another terminal on your computer and keep this SSH tunnel running:
```bash
ssh -N -L 127.0.0.1:5001:127.0.0.1:5001 -L 127.0.0.1:5200:127.0.0.1:5200 @
```
This path requires the test server's `/route` to return the browser-reachable `ws://127.0.0.1:5200`. For another endpoint, configure `api.external_ws_addr` using [Networking](/en/server/configuration/networking) and restart. Tunnels forward ports; they do not rewrite route responses.
Check readiness from the computer running Node.js before starting the example:
```bash
curl --fail http://127.0.0.1:5001/readyz
```
### Start the example on your computer [#start-the-example-on-your-computer]
```bash
git clone --depth 1 https://github.com/WuKongIM/WuKongIM.git WuKongIM-web-example
cd WuKongIM-web-example/docs-site/examples/javascript-web-quickstart
npm ci
npm run dev
```
If you already have the repository, enter the same example directory. Open [http://127.0.0.1:5173](http://127.0.0.1:5173) and:
1. Connect Alice and Bob; confirm both sessions are connected.
2. Send from Alice, checking Alice's server send result and Bob's incoming event separately.
3. Disconnect Bob and send another message from Alice.
4. Reconnect Bob; confirm the missing message is restored and displayed once.
The first history sync may briefly be empty; the example retries within a finite budget.
Retry details and a different API endpoint
Person membership is projected asynchronously after a successful SENDACK, so the first history sync can briefly return HTTP 200 with an empty page. The example backend retries an empty latest page (both sequence bounds zero) up to 20 attempts, 250 ms apart. Nonempty results and ordinary cursor pages return immediately. A genuinely empty chat returns an empty list after at most 19 waits: 4.75 seconds plus HTTP request time. Other errors still fail; production integrations need their own consistency and request-deadline policy.
Node.js uses `http://127.0.0.1:5001` by default. Set `WK_DOCS_QUICKSTART_PRODUCT_HTTP_URL` for a different test API endpoint, for example:
```bash
WK_DOCS_QUICKSTART_PRODUCT_HTTP_URL=http://127.0.0.1:15001 npm run dev
```
The browser calls only the example's `/api/development/identity` and `/api/messages/sync`; the BFF calls WuKongIM HTTP API. In production, replace development identity provisioning with your login and authorization system. Clients do not call `/user/token` directly.
See the reconnect recovery result
**① SYNCED** identifies the restored message. **② Sync complete · 1 recovered** confirms recovery finished; display the same message only once.
## Integrate your own frontend [#integrate-your-own-frontend]
The following explains installation, identity, listeners, and sending. Use two independent tabs, iframes, or browser contexts: `WKSDK.shared()` is a singleton, so do not sign in two users in one page context. The build tool must support npm and TypeScript.
### 1. Install [#1-install]
```bash
npm install --save-exact wukongimjssdk@1.3.5
```
When using pnpm or Yarn, pin the same exact version and keep only the lockfile already used by the project.
### 2. Configure identity and endpoint [#2-configure-identity-and-endpoint]
```typescript
import WKSDK, {
Channel,
ChannelTypePerson,
ConnectStatus,
MessageText,
} from 'wukongimjssdk'
// Same-origin example BFF; use bob on Bob's page.
const response = await fetch('/api/development/identity', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uid: 'alice' }),
})
if (!response.ok) throw new Error('identity request failed')
const bootstrap: { uid: string; token: string; websocketUrl: string } =
await response.json()
const sdk = WKSDK.shared()
sdk.config.uid = bootstrap.uid
sdk.config.token = bootstrap.token
sdk.config.addr = bootstrap.websocketUrl
sdk.config.deviceFlag = 1 // Web
sdk.config.debug = false
```
`bootstrap` is a backend response, not an SDK type. The runnable example provides this URL; in your own project, replace it with your authenticated application login endpoint returning these three fields. Use a correctly certified `wss://` endpoint in production.
### 3. Register listeners first [#3-register-listeners-first]
Show complete listeners: connection, incoming messages, and send results
```typescript
const onConnect = (status: ConnectStatus, reasonCode?: number) => {
if (status === ConnectStatus.Connected) {
console.log('WuKongIM connected')
} else if (status === ConnectStatus.ConnectFail) {
console.error('connect failed', reasonCode)
} else if (status === ConnectStatus.ConnectKick) {
console.error('this account was signed in elsewhere', reasonCode)
}
}
const onMessage = (message: any) => {
if (message.send) return // Sending locally also emits a message event.
if (message.content instanceof MessageText) {
console.log(`${message.fromUID}: ${message.content.text}`)
}
}
const onMessageStatus = (ack: any) => {
if (ack.reasonCode === 1) {
console.log('message sent', ack.clientSeq, ack.messageSeq)
} else {
console.error('message failed', ack.clientSeq, ack.reasonCode)
}
}
sdk.connectManager.addConnectStatusListener(onConnect)
sdk.chatManager.addMessageListener(onMessage)
sdk.chatManager.addMessageStatusListener(onMessageStatus)
```
Keep the function references so the page can remove the same listeners during cleanup.
### 4. Connect and send [#4-connect-and-send]
```typescript
sdk.connect()
```
After `ConnectStatus.Connected`, send from Alice to Bob:
```typescript
const message = await sdk.chatManager.send(
new MessageText('Hello, Bob'),
new Channel('bob', ChannelTypePerson),
)
console.log('local message', message.clientMsgNo)
```
The returned Promise gives you the local message. The server result comes through `onMessageStatus`, while Bob's live message comes through `onMessage`; they are separate events.
## Expected result [#expected-result]
1. Alice and Bob both report `ConnectStatus.Connected`.
2. Alice receives a send state with `reasonCode === 1`.
3. Bob prints “Hello, Bob”.
## Clean up the page [#clean-up-the-page]
```typescript
sdk.connectManager.removeConnectStatusListener(onConnect)
sdk.chatManager.removeMessageListener(onMessage)
sdk.chatManager.removeMessageStatusListener(onMessageStatus)
sdk.disconnect()
```
If connection fails, verify that the endpoint is a WebSocket address rather than an HTTP API address, then check page CSP, reverse proxy, and certificate configuration. Continue with [Connection](/en/sdk/javascript/connection) and [Messages](/en/sdk/javascript/messages).
## Troubleshoot by symptom [#troubleshoot-by-symptom]
| Symptom | Check and action |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `npm ci` or build fails | Check the Node.js version and example directory; keep the repository lockfile |
| Development identity request fails | Check `/readyz`, the API endpoint, and port `5001` forwarding from the Node.js computer |
| Identity succeeds but WebSocket fails | Check the browser can reach the `/route` endpoint result, port `5200` forwarding, certificates, and proxy paths |
| Connected but Bob sees no message | Check the peer UID, server send result, and SDK text payload format |
| Reconnect does not restore messages | Inspect `/api/messages/sync`, confirm durable messages exist, and keep the same test UID |
# Core Concepts (/en/sdk/wukongim/concepts)
Learn these terms before writing code. Method names vary by platform, but the meanings stay the same.
## UID: who the user is [#uid-who-the-user-is]
A `UID` is the stable unique identifier for a user in your product, such as `alice` or `user_1001`. WuKongIM does not own registration, passwords, or user profiles; your product system still manages them.
## Token: whether the user may connect [#token-whether-the-user-may-connect]
A `Token` is the credential used to connect to WuKongIM. After the client signs in to your product, the product backend returns the UID, token, and connection endpoint. Do not generate the token in the client or place server management credentials in the app.
## Channel: who receives the message [#channel-who-receives-the-message]
A `Channel` answers **who should receive this message**:
* Direct chat: the Channel ID is the other user's UID and the type is `1`;
* Group chat: the Channel ID is your product's group ID and the type is `2`.
The SDK uses Channels as one model for direct chats, groups, and other message targets.
## Message: one message and its state [#message-one-message-and-its-state]
A sent message normally has three separate outcomes:
1. **Sending:** the message was created and stored locally;
2. **Sent:** the server accepted and acknowledged it;
3. **Received by the peer:** the other client emitted a new-message callback.
Sent does not mean read. Read receipts are a separate product feature.
## Conversation: one item in the chat list [#conversation-one-item-in-the-chat-list]
A `Conversation` is **one item in the chat list**. It usually contains a Channel, the latest message, a timestamp, and an unread count. Messages belong to Channels; Conversations are the current user's view of recent chats.
## Provider: how the SDK asks for product data [#provider-how-the-sdk-asks-for-product-data]
A `Provider` is a callback supplied by your application. When the SDK needs a Channel profile, message history, or conversation data, it invokes the provider. Your app requests that data from the product backend and returns the result to the SDK.
Always complete a provider callback, even when the result is empty or the request failed. Otherwise the SDK may wait indefinitely.
## Next step [#next-step]
Choose a platform and complete its quickstart: [Android](/en/sdk/android), [iOS](/en/sdk/ios), [JavaScript / Web](/en/sdk/javascript), [Flutter](/en/sdk/flutter), or [HarmonyOS](/en/sdk/harmonyos).
# WuKongIMSDK (/en/sdk/wukongim)
WuKongIMSDK is the full client SDK family. It manages more than a network connection: it also maintains the local messages, chats, unread counts, and data needed for offline recovery.
## Where it fits [#where-it-fits]
You will usually work with four kinds of entry point:
* **Connection manager:** connect, reconnect, disconnect, and connection state;
* **Message manager:** send, receive, query, and update messages;
* **Conversation manager:** the chat list, latest messages, and unread counts;
* **Channel manager:** names, avatars, and membership data for direct or group chats.
## Choose a platform [#choose-a-platform]
## Recommended order [#recommended-order]
1. Read [Core Concepts](/en/sdk/wukongim/concepts) to understand UIDs, tokens, Channels, Conversations, and providers.
2. Open the platform quickstart and complete installation, connection, and the first online text message.
3. Continue with connection, messages, conversations, channels, and advanced features as needed.
4. Use the platform API reference to look up methods, and read [Upgrade SDKs](/en/sdk/wukongim/upgrade) before changing a dependency version.
# Upgrade SDKs (/en/sdk/wukongim/upgrade)
Do not publish after changing only a version number. The SDK also owns a local database, listeners, and connection state, so those behaviors must be checked together.
## 1. Record the current version [#1-record-the-current-version]
Keep the current dependency version and lockfile, then list every connection, message, conversation, Channel, provider, and custom-content API used by the app.
## 2. Read the release notes [#2-read-the-release-notes]
Check the target SDK repository's releases, changelog, and migration notes. Look specifically for:
* changed methods or parameters;
* local database migrations;
* message encoding changes;
* new minimum OS or build-tool requirements.
## 3. Upgrade only the SDK dependency [#3-upgrade-only-the-sdk-dependency]
Change the SDK version and refresh the lockfile without upgrading many unrelated dependencies at the same time. This keeps failures easier to diagnose.
## 4. Re-run the core flow [#4-re-run-the-core-flow]
At minimum, verify that:
1. a signed-in user can connect;
2. Alice can send text to Bob;
3. the send state changes from sending to success;
4. Bob receives the message;
5. the client recovers after losing the network;
6. history, conversations, and unread counts remain correct;
7. custom content still encodes and decodes.
## 5. Check local data [#5-check-local-data]
Test with an existing account and realistic data, not only a clean install. If the SDK changes its database schema, confirm that old data opens and decide how failures will be handled.
## 6. Roll out gradually [#6-roll-out-gradually]
Release to a small group first. Watch connection failures, rejected messages, crashes, and local database errors. If you stop the rollout, restore the dependency version and lockfile together.
If the new SDK already changed local data, the old SDK may no longer open it. Verify the real data upgrade and rollback path before release.
# Channel Messaging Layer (/en/server/architecture/channels)
One Channel owns an ordered message log. Active runtimes share bounded reactors/workers; a Channel does not receive permanent goroutines of its own.
## Slot metadata and Channel logs [#slot-metadata-and-channel-logs]
Slot commits `ChannelRuntimeMeta`; Channel replicas persist message bodies. The Slot Leader and Channel Leader may be different nodes.
Full metadata fields and storage boundaries
Channel is the core unit of message routing, ordering, and persistence. Each active channel has independent runtime state and an ordered log, while many channels are hashed across a bounded number of reactors and workers instead of receiving permanent goroutines per channel.
The physical hash slot for a Channel ID owns its `ChannelRuntimeMeta`, which describes how the data plane should run. Channel replicas store actual message records in `pkg/db/message`.
| Metadata field | Constraint |
| --------------------- | --------------------------------------------------------------- |
| `Leader` | node currently allowed to accept channel writes |
| `Replicas` | nodes expected to store channel log data |
| `ISR` | synchronized replicas participating in committed-HW calculation |
| `MinISR` | minimum ISR count required for quorum commit |
| `Epoch` | fences membership changes |
| `LeaderEpoch` | fences Leader changes within one membership epoch |
| `WriteFence` | blocks new writes during migration or failover |
| `RetentionThroughSeq` | authoritative logical retention boundary |
The Slot Leader and Channel Leader can be different nodes. The former commits channel metadata; the latter commits channel message logs.
## Channel write and replication [#channel-write-and-replication]
Followers Pull/Apply/ACK; the Leader combines its local progress with current ISR progress under `MinISR`. The public durable path succeeds only after HW covers the records. A single-node cluster with `MinISR=1` uses the same state machine and durable append.
Full write, forwarding and replication diagram
The public durable send path uses quorum commit: append succeeds only when the Channel Leader's high watermark covers the records. A single-node cluster with `MinISR=1` still uses the same state machine and durable append.
## Ordering and batching [#ordering-and-batching]
One Channel exposes log-sequence order; different Channels can run concurrently with no global message order. Batching stays bounded and preserves epoch, Leader epoch, idempotency and write-fence checks; stale results cannot advance a generation.
Full ordered batching and completion rules
* Commands for one channel enter its authority writer in submission order. Even if completions return out of order, they drain by append sequence.
* Gateway and channelappend can collect adjacent same-channel requests, and `AppendBatch` assigns contiguous sequences in one path.
* Leader storage and follower apply use bounded workers and batching to reduce fsync and scheduling overhead.
* Batching never relaxes epoch, Leader epoch, idempotency-key, or write-fence checks; a stale result cannot advance the current generation.
Different channels can execute concurrently, but one channel's log sequence defines its visible order. There is no global message order across channels.
## ISR and committed high watermark [#isr-and-committed-high-watermark]
Only records at or below HW are committed; history/latest-message reads stay within loaded or durable HW. Learners in `Replicas - ISR` do not count toward HW quorum and cannot become Leader directly; ISR promotion requires progress proof under the current Leader and epoch.
Full ISR, learner and committed-read constraints
The Leader tracks replication progress for each ISR member and computes a committable sequence from `MinISR`. Only records at or below HW are committed. Historical reads and node-local latest-message reads must also remain under the loaded or durable HW.
A learner in `Replicas - ISR` can receive replication and catch up, but it does not participate in HW quorum and cannot be promoted directly to Leader. Promotion into ISR requires proof of progress under the current Leader and epoch.
## Activation and eviction [#activation-and-eviction]
Activate only after resolving authority, opening storage and applying metadata. Idle runtimes unload only when role/progress/lifecycle gates permit; durable messages survive. Later writes, PullHint or metadata changes reactivate; PullHint never replaces authoritative metadata.
Full activation, eviction and wakeup lifecycle
Channel runtimes activate on demand. A node resolves authoritative metadata, opens the message store, and applies the complete meta before accepting append or replication. An idle channel can slow follower pulls and eventually evict its local runtime when role, progress, and lifecycle conditions permit. Durable messages survive runtime unload.
A later append, PullHint, or authoritative metadata change resolves and activates it again. PullHint is only a wakeup and refresh hint; it never replaces authoritative meta.
## Backpressure [#backpressure]
Full queues reject admission or use bounded waits/retries. Reserve post-commit handoff capacity before durable append; if unavailable, return `ErrChannelBusy` first. Replica/RPC failure stays scoped to its target. Larger queues increase worst-case wait, not capacity.
Full pressure-point and backpressure table
| Pressure point | Behavior |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Per-channel mailbox or append queue full | reject new admission with explicit busy/backpressure |
| Durable append workers full | retain bounded wait or retry without blocking storage IO inside the reactor |
| Post-commit handoff full | reserve capacity before durable append; return `ErrChannelBusy` first rather than lose a handoff after commit |
| Follower or remote RPC failure | isolate the affected replica or target and recover with bounded policy |
These limits protect memory and tail latency. Increasing a queue only increases worst-case wait and does not replace capacity planning.
## Large groups and post-commit fanout [#large-groups-and-post-commit-fanout]
Store one committed Channel message; for 100,000-member groups, page subscribers and create bounded delivery plans instead of loading all members. Commit and full receipt remain separate; delivery failures do not roll back logs, and conversation hydration is a separate read.
Full large-group storage and fanout boundaries
The channel log stores one committed message rather than one persistent copy per member. Post-commit work pages through large-group subscribers, resolves online authority per page, and creates bounded delivery plans without loading the complete member set into memory.
Consequently, "the large-group message committed" and "every online member received it" are different events. Delivery failures do not roll back the committed log, and later conversation-directory hydration is a separate read-side operation.
## Migration and failover [#migration-and-failover]
Persist a Slot write fence and drain the old Leader before task/epoch/proof-fenced switching. Reopening requires newer authoritative metadata; local TTL cannot reopen writes. Conflicting replica/ISR/epoch/Leader evidence keeps append fail-closed. Follow [Scaling](/en/server/operations/scaling).
Full migration and failover fences
A Leader transfer or replica replacement first sets a durable write fence in Slot metadata, drains the old Leader, and uses task-, epoch-, and proof-fenced commands to switch Leader or ISR atomically. Clearing the fence also requires newer authoritative metadata; a node-local TTL does not reopen writes automatically.
If replica, ISR, epoch, or Leader evidence conflicts, Channel append fails closed. See [Scaling](/en/server/operations/scaling) for the operator workflow.
## Source entry points [#source-entry-points]
Follow [Message Send Flow](/en/server/architecture/message-flow) to place Channel commit in the full client path.
Full Channel source entry-point table
| Goal | Entry point |
| ---------------------------------- | -------------------------------------------- |
| Public Channel contracts | `pkg/channel` |
| Multi-reactor state machine | `pkg/channel/reactor`, `pkg/channel/machine` |
| Message storage adapters | `pkg/channel/store`, `pkg/db/message` |
| Metadata resolution and forwarding | `pkg/cluster/channels` |
| Product write authority | `internal/runtime/channelappend` |
Continue with [Message Send Flow](/en/server/architecture/message-flow) to place Channel commit inside the complete client path.
# Controller Layer (/en/server/architecture/controller)
Controller stores low-frequency cluster plans. Raft commits changes before they become materialized state; per-message traffic and user heartbeats stay outside this layer.
## Voters and mirrors [#voters-and-mirrors]
**Voters** participate in Controller Raft; **mirrors** sync a voter's complete state file. A joined data node is not automatically a voter: promotion needs learner stages, membership proof and a final state command.
Complete node-role and promotion rules
Controller is authoritative for low-frequency cluster intent: node membership, logical Slot Group assignment, desired replicas, control tasks, and a small amount of cluster-wide planning state converge here. It does not process every message or user heartbeat.
| Node role | Behavior | State source |
| ------------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------- |
| Controller voter | participates in Controller Raft, commits commands, applies the state machine | local Raft WAL, snapshots, and materialized state |
| Non-Controller data node | does not join Controller Raft and mirrors a voter's complete state file | Controller state sync |
Only nodes explicitly promoted to Controller voter participate in this Raft Group. A dynamically joined data node does not become a voter automatically; promotion has a separate learner, membership-proof, and final-state command path.
## Commit and materialization order [#commit-and-materialization-order]
Persist WAL before sending Raft messages. Committed entries apply in FIFO batches with one atomic state save. WAL is authoritative; `cluster-state.json` is the materialized view. Recovery loads it or a snapshot, then replays the committed suffix after its applied boundary.
Complete commit, materialization and replay flow
The Controller Raft WAL is authoritative for committed commands and applied-boundary metadata. `cluster-state.json` is the materialized view of current business state. Startup loads that view, or restores it from a Raft snapshot, and then replays the committed WAL suffix after the materialized applied index.
## Revision and AppliedRaftIndex [#revision-and-appliedraftindex]
`Revision` versions business state and fences plans/tasks; `AppliedRaftIndex` marks materialized entries. Health/readiness metadata can advance the latter without a new business Revision or topology change.
Full revision and applied-boundary semantics
* `Revision` is the logical cluster-state version used by plans, tasks, and compare-and-set fences.
* `AppliedRaftIndex` is the newest Raft entry materialized into the state file.
* Health reports and non-mutating readiness probes can advance applied Raft metadata without increasing business `Revision`.
An increasing Raft index therefore does not necessarily mean topology changed, and heartbeat churn is not a new resource plan.
## What Controller stores [#what-controller-stores]
Store nodes, assignments, versioned hash-slot mappings and bounded control plans. Channel payloads, concrete Sessions, every Ping, unbounded audit logs and raw diagnostic artifacts belong elsewhere.
Complete stored-state and exclusion list
* node IDs, addresses, roles, and lifecycle;
* logical Slot Raft Groups, desired members, configuration epochs, and preferred leaders;
* the versioned mapping from physical hash slots to logical Slot Groups;
* bounded tasks such as Slot migration and Controller voter promotion;
* bounded low-frequency cluster state such as backup plans and Operations MCP enablement.
It does not store Channel message payloads, concrete TCP Sessions, every user Ping, unbounded audit logs, or raw diagnostic artifacts. High-frequency or unbounded data stays in the data plane, node-local runtime, or external storage.
## Intent is not live fact [#intent-is-not-live-fact]
`DesiredPeers` and `PreferredLeader` describe intent. Actual Leader, term, membership, commit and progress come from live Slot evidence.
A plan may not have converged, and the target can be inactive or behind. Reads and operational decisions must use fresh observed Raft evidence and preserve unknown when it is missing.
Full intent and observed-authority rules
Controller `DesiredPeers` and `PreferredLeader` values are desired state. Actual Slot Raft voters, learners, Leader, term, commit, and synchronization progress come from live Slot runtime evidence.
## Tasks and fences [#tasks-and-fences]
Example: moving a Slot replica.
Every phase carries task/Slot/epoch/attempt/phase fences. Final assignment requires live voter/learner proof before updating `DesiredPeers` and epoch. Scale-in must prove Gateway, Slot, Channel and task drain before `leaving` becomes `removed`.
Full task phases, proof and scale-in fences
Controller tasks split high-risk changes into verifiable phases. A Slot replica move, for example, proceeds through opening a learner, adding it, catching up, promoting it, removing the old voter, and committing the final assignment. Every phase carries fences such as task ID, Slot ID, configuration epoch, attempt, and phase index.
The final command updates `DesiredPeers` and increments the configuration epoch only when live voter and learner evidence matches the intended set. Node scale-in similarly requires higher layers to prove Gateway, Slot, Channel, and task drain before Controller can change a `leaving` node into a `removed` tombstone.
## Failure and recovery [#failure-and-recovery]
No Leader means a write error, never direct state-file edits. Voters recover snapshot + WAL suffix; mirrors sync full state files. Compact only materialized boundaries. Manager observations remain node-local Raft evidence.
Complete failure and recovery boundaries
* Without a Controller Leader, writes return leadership or lifecycle errors instead of editing the local state file directly.
* Voters recover from a snapshot plus WAL suffix; mirrors catch up through complete state-file sync.
* Snapshot and compaction trim only log boundaries covered by materialized state.
* A Manager view of one node's Controller log or status is node-local Raft evidence, not a fabricated global log.
## Source entry points [#source-entry-points]
Continue with the [Slot Metadata Layer](/en/server/architecture/slots) to see how Controller mappings and desired membership become live metadata routes.
Implementation modules and source paths
| Goal | Entry point |
| ------------------------------ | ---------------------- |
| Controller public runtime | `pkg/controller` |
| State model and validation | `pkg/controller/state` |
| Raft WAL, apply, and snapshots | `pkg/controller/raft` |
| Production cluster adapter | `pkg/cluster/control` |
# Architecture (/en/server/architecture)
Start with three responsibilities: Controller plans the cluster, Slot owns metadata, and Channel owns ordered message logs. Gateway, online routing and Transport complete client communication.
A one-node deployment is a single-node cluster and still follows Controller, Slot, Channel, and routing semantics. The default route table keeps 256 stable physical hash-slot fences. They map onto a potentially different number of logical Slot Raft Groups and are not the same concept.
## Architecture map [#architecture-map]
These arrows show assignment and metadata constraints. The per-message SEND path is explained in [Message Send Flow](/en/server/architecture/message-flow); online delivery runs after commit.
Full architecture map and both planes
WuKongIM places low-frequency cluster intent, sharded metadata, ordered message logs, and high-frequency connection state behind different boundaries. Those boundaries matter more than memorizing one function or in-process call graph: failover, scaling, and diagnosis all depend on which component is authoritative for which state.
The diagram contains two distinct paths: Controller and Slot provide control and metadata authority, while Channel provides message-log authority. Online delivery runs after commit and is not part of Channel quorum commit.
## Five primary boundaries [#five-primary-boundaries]
Node assignments and controlled tasks.
Users, Channels and subscriptions.
Order, replication and committed reads.
Sessions, presence and owner-node delivery.
Bounded Raft, RPC and replication transport.
Full responsibility and authority comparison
| Boundary | Authoritative state | Main responsibility | Does not own |
| -------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------ |
| Controller | committed Controller Raft log and applied boundary | nodes, Slot assignment, tasks, cluster-wide plans | high-frequency sessions or Channel payloads |
| Slot | metadata state machine under the current Slot Raft Leader | users, channels, subscribers, ordinary/CMD memberships, ChannelRuntimeMeta | Channel message logs |
| Channel | Channel Leader, ISR, and committed high watermark | per-channel order, durability, replication, committed visibility | global membership plans or concrete TCP sessions |
| Gateway and online runtime | connection owner node and the UID Presence Authority | client sessions, online endpoints, cross-node delivery | durable presence or message-commit decisions |
| Transport | bounded connections, queues, and service entry points | node Raft, RPC, replication, and notification transport | business authorization or unlimited retries |
## Four layers of authority in one route [#four-layers-of-authority-in-one-route]
Controller declares desired assignments; the live Slot Leader/term/configuration epoch governs metadata, while Channel Leader/ISR/epochs govern message writes. Only the owner of the concrete Session can write to that client. `PreferredLeader` is intent; missing live authority or freshness stays unknown.
All four authority layers and freshness rules
A business request can encounter four kinds of authority, and none substitutes for another:
1. **Controller intent** declares desired node and Slot assignments, tasks, and revisions.
2. **Live Slot authority** uses the observed Raft Leader, term, and configuration epoch to decide who can commit metadata.
3. **Live Channel authority** uses the Leader, ISR, Channel Epoch, and Leader Epoch in `ChannelRuntimeMeta` to fence message writes.
4. **Connection ownership** means only the node holding the concrete Session can perform the final client write.
`PreferredLeader` is Controller intent. The observed Raft Leader is the runtime authority. If Manager or diagnostics lack a live term, configuration epoch, or freshness evidence, preserve an unknown result instead of filling the gap from intent.
## Where data lives [#where-data-lives]
Plans live in Controller Raft, metadata in Slot storage and messages in Channel replicas. Sessions/presence, queues and caches live in memory; they follow leases, bounded capacity and rebuildable lifecycles.
Complete storage and lifecycle table
| Data | Typical location | Lifecycle |
| ----------------------------------- | -------------------------------------------- | ----------------------------------------------- |
| Cluster plans and tasks | Controller Raft plus materialized state file | low-frequency, durable, cluster-wide |
| Sharded business metadata | Slot Multi-Raft plus `pkg/db/meta` | durable and partitioned by physical hash slot |
| Channel messages | Channel replicas plus `pkg/db/message` | durable and ordered per channel |
| Connections and online endpoints | owner node and Presence Authority memory | high-frequency, lease- and Session-bound |
| Transport queues and runtime caches | node process memory | bounded, rebuildable, protected by backpressure |
## Continue reading [#continue-reading]
Follow a message in [Message Send Flow](/en/server/architecture/message-flow), find a Session in [User Connection Routing](/en/server/architecture/user-routing), or investigate an incident with [Troubleshooting](/en/server/operations/troubleshooting) and [Diagnostics](/en/server/tools/diagnostics).
Complete architecture reading list
* [Controller Layer](/en/server/architecture/controller): cluster intent, materialized state, and task fences.
* [Slot Metadata Layer](/en/server/architecture/slots): how 256 physical hash slots map to logical Raft Groups.
* [Channel Messaging Layer](/en/server/architecture/channels): Channel Leader, ISR, HW, and message order.
* [Transport Layer](/en/server/architecture/transport): client ingress versus node-to-node transport.
* [Message Send Flow](/en/server/architecture/message-flow): SEND through SENDACK and asynchronous delivery.
* [User Connection Routing](/en/server/architecture/user-routing): local Sessions, Presence Authority, and cross-node push.
To apply these boundaries to an incident, continue with [Troubleshooting](/en/server/operations/troubleshooting) and [Diagnostics](/en/server/tools/diagnostics).
# Message Send Flow (/en/server/architecture/message-flow)
A durable SEND crosses entry adaptation, business permissions, Channel authority, Channel quorum, and post-commit effects. The critical boundary is that durable commit decides SENDACK success; online delivery and webhooks are not part of that synchronous transaction.
## Complete flow [#complete-flow]
This diagram covers default durable messages. Delivery is scheduled separately in [Post-commit effects](/en/server/architecture/message-flow#5-post-commit-effects); `NoPersist` follows the branch below.
Complete send and post-commit implementation flow
## 1. Gateway ingress [#1-gateway-ingress]
Gateway decodes TCP/WebSocket frames and admits work through bounded queues. A full queue, closed Session or invalid frame fails before the use case; Gateway neither chooses the Channel Leader nor writes message storage.
Full ingress, adapters and batching
A client TCP or WebSocket Session sends a WKProto `SEND`. `pkg/gateway` handles frame decoding, connection-level admission, same-channel micro-batching, and bounded asynchronous dispatch. `internal/access/gateway` maps protocol fields into an entry-independent send command.
Gateway does not choose the Channel Leader or write the message database. A full queue, closed connection, or invalid frame fails explicitly before business-usecase admission.
## 2. Permissions and normalization [#2-permissions-and-normalization]
Enforce current Channel/subscription/allow-deny/stranger permissions and optional `BeforeSend`, and normalize identities. Batch results remain item-aligned: rejected items never enter append.
Full permission and normalization rules
`internal/usecase/message` enforces current business permissions: system users, channel state, subscriptions, allowlists, denylists, stranger policy, and optional plugin `BeforeSend`. Direct or command-channel identities are normalized for the request, but the usecase imports no Gateway, Cluster, or Channel implementation type.
Batch send preserves item-aligned results. Rejected items never enter append; only allowed items reach the configured submitter.
## 3. Channel Append Authority [#3-channel-append-authority]
Only the resolved authority node creates the writer; other nodes forward the batch by RPC. The writer revalidates hash slot/Leader/epochs, deduplicates by sender + client message number + Channel, assigns IDs, and preserves order under backlog/worker/handoff limits.
Full writer ownership and admission rules
`internal/runtime/channelappend.Router` resolves a complete authority target for the canonical channel. Only that node creates the product-level append writer. Other nodes forward the whole batch over Channel Append RPC without creating a local proxy writer.
The authority writer:
* revalidates the channel against target hash slot, Leader, and epochs;
* applies idempotency by sender, client message number, and channel;
* assigns message ID and server timestamp;
* stays under per-channel backlog, global worker, and post-commit handoff capacity;
* preserves same-channel item commit and completion order.
## 4. Channel quorum commit [#4-channel-quorum-commit]
Apply current metadata and check `Epoch`, `LeaderEpoch` and `WriteFence`; forward to the Channel Leader as needed. Append succeeds only when HW covers the record, returning its durable ID and sequence.
A successful SENDACK for a default durable message means the authoritative channel path made a durable quorum-commit decision. It does not mean every recipient is online, every client has RECVACKed, a webhook completed, or a business database updated.
If the client disconnects before SENDACK, use a stable `client_msg_no` and query or sync evidence to determine the result. A network timeout alone does not prove the message was not committed.
Complete quorum append and result rules
The Cluster Channel service resolves and applies current `ChannelRuntimeMeta`. If the local node is not Channel Leader, it forwards append to the Leader. The Leader reactor checks `Epoch`, `LeaderEpoch`, and `WriteFence`, then performs local durable append and ISR replication.
The quorum append Future succeeds only when HW covers the records. Each result returns the durable message ID and channel sequence.
## 5. Post-commit effects [#5-post-commit-effects]
Paged subscribers and exact presence routes feed bounded owner-node delivery plans. Webhooks/PersistAfter receive best-effort events from the same commit; one recipient group's failure neither reverses SENDACK nor blocks unrelated targets.
SEND does not write recipient membership or conversations. Clients later page their UID-owned membership directory; Channel-head hydration constructs conversations.
Full subscriber, routing and side-effect steps
A new commit becomes an immutable `CommittedEnvelope` in the authority writer. The following work is scheduled independently of SENDACK:
1. Direct channels derive participants, ordinary channels load a versioned subscriber snapshot, and large groups page by cursor.
2. Recipient UIDs resolve under one route snapshot into exact presence targets.
3. A bounded `RecipientDeliveryPlan` groups by authority target and enters the online-delivery runtime.
4. Online routes coalesce by owner node; local owners write Sessions and remote owners receive owner-push RPC.
5. Optional PersistAfter plugins and webhooks receive best-effort events from the same committed boundary.
SEND performs no recipient membership or conversation write. Clients discover
ordinary conversations later by paging their UID-owned membership directory;
the server groups Channel-head hydration by Leader and constructs the response.
Failure in one recipient group does not change a successful SENDACK or block unrelated targets. Stage-specific logs and metrics expose the failure.
## 6. Receive and acknowledgement [#6-receive-and-acknowledgement]
Before RECV, revalidate UID, Boot/Session ID and owner fences and bind bounded ACK state. RECVACK removes only matching owner-local pending state; Session close clears that Session. Receive ACKs do not alter committed history or Channel order.
Full receive identity and ACK lifetime rules
Before writing `RECV`, the owner node revalidates UID, Boot ID, Session ID, and owner fences and binds bounded ACK state when acknowledgement is required. Client `RECVACK` removes only the matching owner-local pending identity; Session close removes state for that Session.
Durable committed history and whether one client has RECVACKed are separate dimensions. Receive acknowledgement cannot rewrite or change Channel commit order.
## NoPersist branch [#nopersist-branch]
**Ordinary:** without `SyncOnce` or a command ID, retain the source Channel and use a transient ID with sequence zero. **Command-style:** `SyncOnce` or a command ID maps the command Channel; success requires the online plan to be accepted, and missing delivery capability fails. Both still use authority/writer order, skip durable logs, membership mutation and PersistAfter, and offer no offline recovery or reliable replay.
Full ordinary and command NoPersist branches
An ordinary `NoPersist` send (without `SyncOnce` or a command-channel ID) passes validation and permissions, retains the source Channel, resolves its authority, allocates a transient message ID, and enters online delivery in writer order. Its sequence is zero; it skips the durable log, membership mutation, and PersistAfter.
A command-style `NoPersist` send (with `SyncOnce` or an existing command-channel ID) maps to the command channel, resolves authority, and enters online recipient routing in writer order. It skips the Channel durable log, membership state, and PersistAfter, and reports success only after the online delivery plan is accepted. If no online-delivery capability exists, it fails rather than fabricating success.
Neither `NoPersist` branch has a durable sequence or offline-recovery guarantee, so neither is suitable for business events that require reliable replay.
## Common failure boundaries [#common-failure-boundaries]
No SENDACK: inspect Gateway, permissions, authority and HW. SENDACK succeeded but nothing displayed: inspect subscribers, presence, owner push and Sessions. For device, webhook or duplicate symptoms, follow [Troubleshooting](/en/server/operations/troubleshooting) with stage evidence.
Full symptom-to-boundary table
| Symptom | Inspect first |
| -------------------------------------- | ---------------------------------------------------------------------- |
| No SENDACK | Gateway queue, permission, authority route, Channel append and HW |
| SENDACK succeeds but peer sees nothing | subscriber page, presence target, delivery plan, owner push, Session |
| Only some devices receive | exact routes, Boot/Session fences, device conflicts, local write error |
| Webhook fails but message exists | PersistAfter/webhook queue and receiver, not Channel rollback |
| Retry creates a duplicate | stable `client_msg_no`, idempotency scope, timeout-outcome handling |
Use the evidence ladder in [Troubleshooting](/en/server/operations/troubleshooting) to narrow the boundary. Do not infer failed Channel commit directly from a final UI that did not display a message.
## Source entry points [#source-entry-points]
Continue with [User Connection Routing](/en/server/architecture/user-routing) to find the concrete Session after commit.
Full source entry-point table
| Stage | Entry point |
| -------------------------------- | ------------------------------------------------------ |
| Client frame and ingress adapter | `pkg/gateway`, `internal/access/gateway` |
| Permission and send usecase | `internal/usecase/message` |
| Product append authority | `internal/runtime/channelappend` |
| Cluster and Channel commit | `pkg/cluster/channels`, `pkg/channel` |
| Online delivery and ACK | `internal/runtime/delivery`, `internal/runtime/online` |
Continue with [User Connection Routing](/en/server/architecture/user-routing) to see how post-commit delivery finds a concrete Session.
# Slot Metadata Layer (/en/server/architecture/slots)
Slot partitions and replicates business metadata. Resolve a key to its logical Raft Group and observed Leader before reading or writing authoritative state.
## Do not mix the two Slot concepts [#do-not-mix-the-two-slot-concepts]
* **Physical hash slots: 256 by default.** Stable key-space fences.
* **Logical Slot Raft Groups: `initial_slot_count`.** Each replicates metadata for one or more physical hash slots.
Ordinary scaling moves Group replicas/Leaders; mapping changes require explicitly enabled, controlled hash-slot migration.
Full physical/logical Slot comparison
The Slot layer is the distributed metadata store. Users, channel definitions, subscriptions, UID-owned ordinary/CMD memberships, and `ChannelRuntimeMeta` are partitioned by stable physical hash slot and replicated through logical Slot Raft Groups.
| Name | Default count | Purpose |
| ----------------------- | -------------------------------- | ------------------------------------------------------------------------------- |
| Physical hash slot | 256 | stable key-space fence; UIDs and Channel IDs hash here first |
| Logical Slot Raft Group | selected by `initial_slot_count` | Raft replication and metadata state machine for one or more physical hash slots |
Scaling normally moves replicas or Leaders of logical Slot Groups without changing the 256 physical hash slots. The mapping changes only through an explicitly enabled and controlled hash-slot migration.
## From a key to the authoritative Leader [#from-a-key-to-the-authoritative-leader]
Resolve keys under one immutable, atomically replaced route snapshot. Missing mapping or observed Leader returns not-ready/unknown, never an arbitrary node. The exact Leader identity and route revision fence routing.
Full key routing and authority-target diagram
The route table is an immutable snapshot replaced atomically, so hot paths can resolve many keys under one version. Missing routes, mappings, or observed Leaders return not-ready or unknown instead of falling back to an arbitrary node.
## Metadata stored by Slot [#metadata-stored-by-slot]
Store users/devices, channels/subscriptions, ordinary membership and separate CMD bindings, plus Channel runtime constraints. Message payloads stay in [Channel logs](/en/server/architecture/channels/).
Complete metadata and membership list
* users, devices, and system UIDs;
* channel definitions, subscribers, allowlists, denylists, and membership;
* ordinary membership join/hide/badge/activation state and separate CMD bindings;
* Channel Leader, Replicas, ISR, epochs, write fences, and retention boundary;
* Channel migration tasks, plugin bindings, and bounded message-event projections.
Message payloads do not belong to Slot. The [Channel Messaging Layer](/en/server/architecture/channels) stores them in separate per-channel logs.
## Metadata writes [#metadata-writes]
After quorum, the FSM verifies physical hash-slot ownership and commits a single metadata WriteBatch atomically. Advance the durable applied boundary before completing the Future. A single-node cluster uses one voter and keeps Raft/FSM.
Complete routing, quorum, FSM and apply steps
1. The caller hashes the key to a physical hash slot and resolves its logical Slot Group from one route snapshot.
2. A local current Leader enters `Runtime.Propose`; otherwise a bounded RPC forwards to the resolved Leader.
3. Multi-Raft persists and replicates the entry, then gives committed entries to the Slot FSM after quorum.
4. The FSM verifies that each physical hash slot belongs to the logical Group and applies a batch into one `pkg/db/meta` WriteBatch.
5. The WriteBatch commits atomically, then the durable applied boundary advances and the Future completes.
A single-node cluster keeps this path. Its quorum is one voter; it does not bypass Raft or the FSM.
## Authoritative and local reads [#authoritative-and-local-reads]
Local reads expose only explicitly allowed node-local/applied state. Authoritative reads resolve the Group, try peers and follow fresh `not_leader` hints.
One node's Pebble data, Raft log, or Slot status describes that node's local boundary. Manager aggregation must retain node, term, configuration epoch, and freshness instead of presenting one node's read as a globally consistent snapshot.
Full local and authoritative read boundaries
A local replica can serve explicitly node-local or applied-state reads. Reads requiring current authority resolve the logical Slot Group, try its peers, and follow a `not_leader` response toward the newest Leader hint.
## Preferred Leader and actual Leader [#preferred-leader-and-actual-leader]
Preferred Leader is Controller intent; current Leader/term/membership/commit/progress come from live Multi-Raft status. Transfer needs fresh intent, exact membership, recent activity and catch-up through commit. Stale/joint/lagging/inactive state keeps the current assignment.
Full preferred-Leader convergence gates
Controller stores desired peers, configuration epoch, and preferred leader for each logical Group. Live Slot Multi-Raft status supplies the current Leader, term, voters, learners, commit, and match progress.
Background preferred-leader convergence attempts a transfer only when intent is still current, membership matches exactly, the target is recently active, and it has caught up through commit. Stale intent, joint configuration, lag, or inactivity preserves the current state; the reconciler does not pick a different convenient candidate.
## Replica movement and recovery [#replica-movement-and-recovery]
Add a learner, catch up, promote, remove the old voter, then commit assignment/epoch. Retain live Raft proof at every stage; failure preserves current membership and observable state. Recovery installs the snapshot before replaying its committed suffix; do not edit state files or local metadata.
Complete replica movement and snapshot recovery
A safe replica move adds the target as a learner, waits for catch-up, promotes it, removes the old voter, and finally lets Controller commit the new assignment and configuration epoch. Snapshots let a new learner cross compacted history; recovery installs the snapshot first and replays committed entries after its index.
Every stage retains live Raft proof. A failed task preserves current membership and observable state instead of editing `cluster-state.json` or local metadata directly.
## Capacity and backpressure [#capacity-and-backpressure]
Schedulers, workers and queues remain bounded; batches limit records, bytes and wait. Large snapshots are chunked and reassembled. Queue saturation, Leader change or route-revision mismatch returns explicit failure or bounded retry, never silent loss.
Complete scheduler, batch and backpressure limits
* Multi-Raft uses a bounded scheduler and workers across Groups; a hot Group does not receive an unbounded queue.
* FSM batching amortizes storage commits while preserving record, byte, and wait limits.
* Large snapshots are chunked in cluster transport and reassembled before entering the Slot runtime.
* Queue saturation, Leader changes, and route-revision mismatch return explicit errors or bounded retries instead of silently losing metadata writes.
## Source entry points [#source-entry-points]
Continue with the [Channel Messaging Layer](/en/server/architecture/channels) to see how Slot metadata fences each channel's message replicas.
Implementation modules and source paths
| Goal | Entry point |
| ------------------------------ | ------------------------------------------ |
| Route table | `pkg/cluster/routing` |
| Slot Multi-Raft | `pkg/slot/multiraft` |
| Metadata FSM and proxy | `pkg/slot/fsm`, `pkg/slot/proxy` |
| Local metadata database | `pkg/db/meta` |
| Controller-to-Slot composition | `pkg/cluster/slots`, `pkg/cluster/propose` |
# Transport Layer (/en/server/architecture/transport)
Clients use Gateway; cluster nodes use Transport. Their ports, protocols, identities and capacity limits are separate.
## Two network planes [#two-network-planes]
Gateway and node Transport have separate ports, protocols, identities and capacity limits. HTTP API and Manager are additional service entries; client tokens, Manager JWTs, Join Tokens and discovery addresses serve different purposes.
Full network-plane and protocol comparison
WuKongIM has two easily confused network boundaries. Clients enter through Gateway TCP/WebSocket, while cluster nodes use a separate node Transport for Controller, Slot, Channel, and internal RPC traffic. They have different ports, protocols, identities, and capacity limits.
| Plane | Peer | Primary protocol | Entry point |
| -------------- | ------------------------------------ | --------------------------------- | ------------------------------------------- |
| Client Gateway | SDK, Chat Demo, terminal application | WKProto over TCP/WebSocket | `pkg/gateway`, `pkg/gateway/transport/gnet` |
| Node Transport | WuKongIM nodes | framed TCP, typed RPC/notify/Raft | `pkg/transport`, `pkg/cluster/net` |
HTTP API and Manager are separate service entries again. Do not treat client tokens, Manager JWTs, Join Tokens, and node discovery addresses as one authentication mechanism.
## Node transport structure [#node-transport-structure]
`Call` uses request/response; `Send` is one-way, and Raft uses ordered lanes. Fixed service IDs select the cluster capability; generic Transport does not interpret business payloads. Delivery to a remote service does **not** prove state-machine commit.
Full RPC, notification and ordered-lane diagram
Each frame carries kind, priority, service ID, request ID, and body length. Cluster adapters assign fixed service IDs to Controller Raft, Slot Raft, state sync, Channel replication, foreground append, online owner push, and other bounded capabilities. Generic Transport does not interpret business payloads.
## Listen and advertised addresses [#listen-and-advertised-addresses]
`listen_addr` binds this node; `advertise_addr` or static peer addresses must be stable and reachable by other nodes. Discovery maps node IDs to bounded connection pools. Seed/static addresses aid startup; Controller snapshots provide the continuing membership view. See [Networking & Client Access](/en/server/configuration/networking).
Full bind, advertisement and discovery rules
* `listen_addr` controls the local bind and can use `0.0.0.0` or `[::]` to accept connections.
* `advertise_addr`, or an address in the static node table, must be a stable endpoint reachable by every peer.
* Discovery resolves node IDs to remote addresses and opens a limited connection pool per peer.
* Controller mirrors, dynamic join, and early startup can use seed or static addresses, while Controller snapshots provide the continuing membership view.
Publishing a bind address produces a node that can listen locally but cannot be reached by peers. See [Networking & Client Access](/en/server/configuration/networking) for configuration details.
## Bounded queues are correctness boundaries [#bounded-queues-are-correctness-boundaries]
Frame size, queued items/bytes, batches, handler concurrency and write timeouts are bounded. Saturation returns backpressure or closes a failed connection, waking waiting RPCs.
Increasing a network queue absorbs a larger burst but also raises memory and worst-case queue time. Sustained overload requires lower input, more capacity, or a fixed slow service; it must not be hidden behind an unbounded queue.
Complete queue, batch and write limits
Transport limits frame body size, queued items and bytes per connection, batch frames and bytes, service queues, handler concurrency, and write timeouts. Saturation returns backpressure or closes a failed connection so waiting RPCs wake explicitly.
## Priority and ordering [#priority-and-ordering]
Raft/control traffic keeps low wait and Raft handlers preserve order. Append concurrency and same-target Pull batches remain bounded and retain item results and fences. Priority changes scheduling, never authority checks or the outcome of a timeout.
Full priority, batching and ordering rules
* Raft and control messages keep low wait and do not join the short coalescing window used by ordinary RPC traffic.
* Foreground Channel append services can use higher bounded concurrency while retaining payload and queue limits.
* Raft protocol services use an ordered handler lane so concurrent handlers cannot reorder peer messages.
* Channel Pull and PullHint can form small same-target batches while preserving item-aligned results and channel fences.
Priority affects scheduling only. It does not bypass authority checks or turn a timed-out request into a known non-execution.
## Failure semantics [#failure-semantics]
Backpressure calls for throttling or bounded retry. RPC timeout leaves the result potentially unknown: retry only with the operation's idempotency/fence contract. Unknown services or version mismatches require compatibility checks; Slot/Channel re-resolve authority, while Transport does not guess another Leader.
Complete failure semantics and caller actions
| Failure | Caller behavior |
| ------------------------------------- | -------------------------------------------------------------------------------- |
| node absent from discovery | preserve route-not-ready and refresh the control view |
| dial or connection failure | reconnect inside a bounded budget and retain node/service context |
| queue backpressure | throttle or retry later without unbounded accumulation |
| RPC context timeout | outcome may be unknown; retry only operations with idempotency and fences |
| service not found or version mismatch | stop and check artifact compatibility rather than selecting an arbitrary service |
| peer closes | reader shutdown wakes pending RPCs instead of leaving them blocked |
Slot and Channel layers re-resolve Leader or epoch before deciding whether to retry. Transport does not guess a new authority node.
## Security boundary [#security-boundary]
Node Transport belongs on a controlled cluster network, with addresses supplied by trusted configuration or Controller state. The current node transport is internal framed TCP and should not be exposed directly to the internet. Crossing an untrusted network requires a reviewed private network, tunnel, or equivalent outer protection.
Browsers and clients must not access node Transport. WuKongIM HTTP API, Manager, Gateway, Operations MCP, and Debug each require separate entry and least-privilege controls; see [Security & Access](/en/server/configuration/security).
## What to observe [#what-to-observe]
Observe peer connections, queue items/bytes and wait, RPC latency/timeouts, executor capacity and layered Raft/replication errors. Use [Diagnostics](/en/server/tools/diagnostics). Remote service delivery is a transport observation; state-machine commit requires separate evidence.
Complete layered signals and diagnostic boundary
* peer connection count, reconnects, and dial failures;
* scheduler queued items and bytes, batch size, and wait;
* typed RPC latency, timeout, and result;
* service queue, running/capacity, and handler duration;
* layered Channel replication and Raft errors instead of aggregate network throughput alone.
During diagnosis, distinguish "Transport delivered to the remote service" from "the remote state machine committed." The former does not prove the latter.
## Source entry points [#source-entry-points]
Continue with [Message Send Flow](/en/server/architecture/message-flow) to see foreground append and post-commit delivery cross these network boundaries.
Implementation modules and source paths
| Goal | Entry point |
| -------------------------------------- | ------------------------------------------------------------ |
| Generic bounded node Transport | `pkg/transport` |
| Cluster typed RPC and discovery | `pkg/cluster/net` |
| Gateway client Transport | `pkg/gateway/transport` |
| Controller, Slot, and Channel adapters | `pkg/cluster/control`, `pkg/cluster`, `pkg/cluster/channels` |
# User Connection Routing (/en/server/architecture/user-routing)
The Gateway owner holds the real client connection. Presence Authority finds it through bounded, expiring memory routes; message commit and online delivery are separate stages.
## Two layers of connection state [#two-layers-of-connection-state]
The **owner node** holds the concrete Session; **Presence Authority** holds routes for its current physical hash slots. Online routes stay in memory and are rebuilt from Gateway activity and reconnects; they are not written into Slot Raft.
Full state ownership and memory boundaries
A client connection lives on the Gateway node that accepted it, while post-commit delivery can run on a different Channel Leader node. User routing locates a UID's concrete connection owner through bounded, expiring memory state and prevents old Leaders, processes, or Sessions from receiving new traffic.
| Layer | Stored state | Authority scope |
| --------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `internal/runtime/online` | this node's pending/active Sessions, OwnerRoutes, and concrete write/close handles | a real client connection belongs only to its owner node |
| `internal/runtime/presence` | in-memory UID to virtual owner-route directory | only physical hash slots currently led by this node |
Presence Authority does not hold concrete TCP Sessions or write online routes into Slot Raft. It is a high-frequency memory view rebuilt from Gateway activity and client reconnects.
## Connection activation [#connection-activation]
Register a local pending Session before its route. Failure rolls back registration; a device conflict requires acknowledged old-owner action before the candidate commits. `MarkActive` then rechecks that the concrete Session still exists.
Complete activation and device-conflict flow
Activation registers a local pending Session before registering its route with the current UID authority. Registration failure rolls back local state. Device conflicts require acknowledged old-owner actions before the pending route can commit. The final `MarkActive` rechecks that the Session still exists, closing the race where authority accepted a route after the local connection disappeared.
## Authority target fence [#authority-target-fence]
Register, touch, lookup and unregister validate the exact hash slot, Slot Group, Leader, term and configuration epoch. A new term/epoch clears old state; a revision-only change under the same Raft identity can retain it. Lost authority rejects stale targets.
Exact target fields and freshness rules
Every registration, touch, lookup, and unregister carries an exact target:
```text
(HashSlot, SlotID, LeaderNodeID, LeaderTerm, ConfigEpoch)
```
The routing revision accompanies the target as freshness evidence. When a node gains authority for a physical hash slot, it installs a fresh identity. A changed Leader term or configuration epoch clears old active and pending state. A route-revision-only update under the same Raft identity can retain the directory while advancing its observed revision.
Losing authority removes all memory state for that hash slot. Old calls receive not-leader or stale-target instead of serving expired routes.
## Route identity [#route-identity]
`BootID` distinguishes node processes, `SessionID` distinguishes connections, and monotonic `OwnerSeq` protects newer unregister tombstones from delayed register/touch. UID plus node ID alone cannot identify a safe delivery target.
Complete route identity fields
An online endpoint carries at least:
* `UID`, `DeviceID`, Device Flag, and Device Level;
* owner `NodeID` and process `BootID`;
* `SessionID` and listener;
* monotonic `OwnerSeq`, connection time, and last activity.
`BootID` separates processes that reuse a node ID, `SessionID` separates connections, and `OwnerSeq` prevents delayed register or touch from overwriting a newer unregister tombstone. UID plus node ID alone could deliver into a restarted or replaced connection.
## Touch and TTL [#touch-and-ttl]
Ping marks owner-local activity. Bounded batches renew current routes; only still-current failed routes are requeued. Expiry scans due buckets and removes active routes while preserving unregister tombstones and monotonic owner sequence.
An online result means authority still sees a route under the current fence and TTL. It does not prove a recent business action or guarantee the next network write. The final owner node still validates the concrete Session.
Full touch batching, expiry and lease behavior
Each client Ping marks dirty activity in the owner-local registry instead of producing an authority RPC. An app worker drains a bounded set, groups by current authority target, and sends `TouchRoutes` batches. Failed routes are requeued only when the same owner route remains current.
Authority uses activity-second buckets and an expiry index, scanning only due buckets instead of every online user. TTL expiry removes an active route but preserves explicit unregister tombstones and monotonic owner sequence.
## Lookup and delivery [#lookup-and-delivery]
This is post-commit online delivery. Subscriber UIDs resolve through the 256 physical hash slots in one route snapshot; lookups group by exact target and pushes form bounded owner-node batches. One stale group leaves successful siblings intact. The final owner validates UID, Boot ID, Session ID and owner sequence; only the sender's exact SEND Session is skipped, so its other devices still receive sync.
Full recipient lookup, push and ACK steps
Post-commit delivery finds recipients as follows:
1. Channel append hashes UIDs in one subscriber page across the 256 physical hash slots.
2. One route snapshot aligns UIDs with complete authority targets and groups by target.
3. `EndpointsByTargets` validates each group fence; one stale group does not erase successful siblings.
4. Returned routes coalesce by owner node into bounded local or remote push batches.
5. A remote owner-push RPC transports routes and frame only; the target owner revalidates UID, Boot ID, Session ID, and owner sequence.
6. The concrete Session writes `RECV` through an entry-independent handle and binds owner-local ACK state when required.
Only the sender's exact SEND Session is suppressed; its other devices can still receive synchronization. Duplicate recipient rows can preserve duplicate-write semantics, while endpoint lookup stays within a bounded plan.
## Device conflicts [#device-conflicts]
For the same UID and Device Flag, Master can replace same-kind devices; Slave generally replaces the same Device ID. Keep the old active route until the old-owner action succeeds, then commit the pending candidate.
Complete device-conflict rules
A new connection with the same UID and Device Flag can conflict with an old route. A Master level can replace same-kind device connections, while a Slave level generally replaces only the same Device ID. Authority retains the old active route and creates a pending candidate until owner action succeeds, avoiding two advertised conflicting routes before the old connection is actually closed.
## Failure behavior [#failure-behavior]
Gateway failure stops touches and routes expire at TTL; a new authority rebuilds its memory directory. During activation, a closed Session queues an exact unregister. Unregister failures retry after local removal; a mismatched final Session rejects push. Tolerate a rebuild window and recover through reconnects plus durable message sync.
Full failure behavior and recovery limits
| Scenario | Behavior |
| -------------------------------- | ----------------------------------------------------------------------------- |
| Authority Leader changes | new authority clears and rebuilds memory routes; old targets are fenced |
| Gateway node crashes | touches stop, routes expire after TTL, and clients reconnect with new routes |
| Session closes during activation | `MarkActive` fails and queues an exact unregister |
| Unregister RPC fails temporarily | owner removes the local Session first and retries the tombstone in background |
| One authority group is stale | that group returns an error while other exact-target groups continue |
| Final Session does not match | owner-local push rejects the route without writing another connection |
Presence is in memory, so failover has a route-rebuild window. Callers must tolerate temporary unknown or offline state and recover through client reconnect plus durable message sync instead of turning online presence into a high-frequency global Raft log.
## Source entry points [#source-entry-points]
Read [Message Send Flow](/en/server/architecture/message-flow) together with this page to separate commit from delivery. For incidents, begin with [Health & Monitoring](/en/server/operations/health-and-monitoring).
Implementation modules and source paths
| Goal | Entry point |
| ----------------------------- | ---------------------------------------- |
| Connection orchestration | `internal/usecase/presence` |
| Owner-local Sessions | `internal/runtime/online` |
| Target-fenced authority | `internal/runtime/presence` |
| Plan execution and owner push | `internal/runtime/delivery` |
| Gateway Session adapter | `internal/access/gateway`, `pkg/gateway` |
# Nodes & Cluster (/en/server/configuration/cluster)
Set identity and joining first, then Slots and replicas. Every deployment follows cluster semantics, including a single-node cluster.
## Required at startup [#required-at-startup]
Every topology requires these three settings:
| Purpose | Setting and constraint |
| ---------------------------------------------------- | ----------------------------------------------------------------- |
| Identity | `node.id`: non-zero, unique in the cluster, and stable |
| Disk | `node.data_dir`: durable directory owned exclusively by this node |
| Transport | `cluster.listen_addr`: local inter-node listener |
**Example scope:** `12` logical groups is a development baseline for the single-node cluster example. Physical hash slots stay at `256`.
Complete required settings and single-node cluster TOML
Every WuKongIM deployment is a cluster. A one-process deployment is a **single-node cluster** and does not bypass cluster semantics. Establish identity and discovery before setting replicas or workload controls.
These three fields are required for every topology:
| TOML | Constraint |
| --------------------- | -------------------------------------------------------- |
| `node.id` | A non-zero, stable node ID that is unique in the cluster |
| `node.data_dir` | A durable data directory owned exclusively by this node |
| `cluster.listen_addr` | The local listener for inter-node Transport |
A minimal single-node cluster can start here:
```toml
[node]
id = 1
data_dir = "/var/lib/wukongim"
[cluster]
listen_addr = "127.0.0.1:7000"
nodes = [{ id = 1, addr = "127.0.0.1:7000" }]
initial_slot_count = 12
hash_slot_count = 256
slot_replica_n = 1
channel_replica_n = 1
```
The `initial_slot_count = 12` value is a development baseline, not a universal recommendation. The `hash_slot_count = 256` value is the stable physical hash-slot fence.
## Choose one join model [#choose-one-join-model]
### Static inventory [#static-inventory]
Use `cluster.nodes` when addresses are known in advance.
### Seed joining [#seed-joining]
Use `cluster.seeds` with a reachable `cluster.advertise_addr` and a non-empty `cluster.join_token`.
**Choose one.** Static inventory cannot mix with seed-join configuration. A listener may bind `0.0.0.0`; remote peer addresses cannot be wildcard, host-only loopback, or short-lived addresses. Replace a single-node cluster's loopback address before adding a remote node.
Complete join fields and address constraints
| Model | Configuration | Boundary |
| ---------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Static inventory | `cluster.nodes` | Node addresses are known in advance and every node uses the same inventory |
| Seed joining | `cluster.seeds`, `cluster.advertise_addr`, `cluster.join_token` | A new node joins through existing nodes and advertises an address peers can reach |
`cluster.nodes` cannot be combined with seed-join configuration. Seed joining requires an advertised address and a non-empty join token. `cluster.listen_addr` may bind `0.0.0.0`. Any `cluster.advertise_addr` or `nodes[].addr` that remote peers must reach cannot use wildcard, host-only loopback, or short-lived addresses. A single-node cluster may use `127.0.0.1`, but it must change to a remotely reachable address before adding a remote node.
## Slots and replicas [#slots-and-replicas]
* Keep `hash_slot_count=256`. `initial_slot_count` creates logical groups only in the first Controller snapshot: initialization and examples use `12`; omitted or `0` uses `1`.
* Existing clusters use the persisted count. Editing configuration does not resize groups; increasing it may block readiness.
* `slot_replica_n` and `channel_replica_n` must fit available nodes. Fault tolerance requires independent machines, disks, and failure domains; processes on one host do not qualify.
After data exists, cluster ID, node IDs, hash-slot count, and replica policy require controlled changes.
Complete Slot counts and replica boundaries
* `initial_slot_count` is the logical Slot Raft Group count created by the first Controller snapshot. `wukongim init` and shipped examples set it to `12`; omitting it or setting it to `0` still uses `1`. Existing clusters retain the persisted count; changing this setting does not resize Slots and increasing it may block readiness.
* `hash_slot_count` is the physical hash-slot fence that routes keys to logical groups; keep it at `256`.
* `slot_replica_n` and `channel_replica_n` must fit the available node count and failure-domain objective.
* Replicas provide the intended fault tolerance only when they span independent machines, disks, and failure domains. Multiple processes on one host are not independent failure domains.
After data exists, do not treat cluster ID, node IDs, hash-slot count, or replica policy as ordinary rolling settings. Joining, migration, and scaling require controlled operations.
## Tuning fields [#tuning-fields]
1. Keep a measured baseline; test production-like users, message rates, channels, and 100,000-member groups.
2. Change one parameter family at a time. Some `0` values derive from hardware or topology; examples do not define every effective value.
3. Compare throughput, P99, queues, CPU, memory, disk, and network, including allocation, contention, and backpressure costs.
Complete tuning fields and workload evidence
`channel_reactor_count`, worker pools, RPC and append batches, commit coordination, and recovery probes affect CPU, memory, allocations, contention, queueing, backpressure, and tail latency. Some zero values are derived from hardware or topology at runtime, so the example does not reveal every effective value.
Keep a measured baseline first. Test with production-like online users, message rates, channel counts, and 100,000-member groups; change one parameter family at a time and record throughput, P99, queues, CPU, memory, disk, and network together.
## Change safely [#change-safely]
Configuration loads at startup. Hot reload requires explicit subsystem support. For each node:
1. Remove traffic and stop the node.
2. Update configuration and restart.
3. Wait for `/readyz` to return `200` with `ready: true`, restore traffic, then continue to the next node.
Complete safe changes and reading paths
Configuration is loaded at node startup; do not assume general hot reload unless a subsystem explicitly documents it. Change cluster configuration one node at a time: remove traffic, stop the node, update configuration, start it, wait for `/readyz`, restore traffic, and only then continue.
Read [Slot Architecture](/en/server/architecture/slots) to see how 256 physical hash slots route into logical Slot Raft Groups, then continue with [Networking & Client Access](/en/server/configuration/networking) to separate listen, peer-advertised, and client-advertised addresses.
# Common Configurations (/en/server/configuration/common-configurations)
Choose a task first. `WK_` environment values override TOML; expand the complete field, environment, and defaults reference below.
A one-node deployment is a single-node cluster. Every scenario requires `node.id`, `node.data_dir`, and `cluster.listen_addr`; configuration changes usually require a node restart.
## Frequently used settings [#frequently-used-settings]
### Start a node [#start-a-node]
| Use | Setting and result |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Identity | `node.id`: Non-zero and unique; never change after data exists |
| Storage | `node.data_dir`: Exclusive, writable storage on a persistent, backed-up disk |
| Peers | `cluster.listen_addr`: Local listener; peers need real reachable addresses, never wildcards |
Choose static `cluster.nodes` or non-empty `cluster.seeds`. JSON environment lists **replace the whole list**.
### Connect clients [#connect-clients]
| Use | Setting and result |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Clients | `gateway.listeners`: Omission listens on all interfaces: TCP `5100` / WSMux `5200`; an explicit empty list disables Gateway |
| API | `api.listen_addr`: Omitted or empty disables WuKongIM HTTP API; configure Gateway and Manager separately |
Keep `gateway.token_auth_on=true`. CONNECT exactly matches the stored device token for the same `UID + device_flag`; empty, missing, or mismatched tokens return `ReasonAuthFail`. WuKongIM HTTP API still needs a trusted network or external auth.
### Open Manager [#open-manager]
| Use | Setting and result |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Listener | `manager.listen_addr`: omitted or empty disables Manager; use loopback / private networks with SSH or HTTPS |
| JWT | `manager.auth_on`: omission defaults to `false`; set `true` in production with `manager.jwt_secret` and `manager.users` |
**Verify:** replace example credentials, sign in, and grant only required resources and actions.
### Inspect logs [#inspect-logs]
`log.level` defaults to `info`; choices are `debug`, `info`, `warn`, and `error`. Use `debug` temporarily for diagnosis, then restore the previous level.
Complete common fields, defaults, and environment mappings
This page keeps only the settings most often consulted for startup, client access, and daily operations. Each setting can be provided through TOML or an environment variable; when both are present, the environment variable overrides the file value. See the [Configuration Reference](/en/server/configuration/reference) for every field and its exact semantics.
| Setting (TOML / environment) | Purpose | Defaults and cautions |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`node.id`](/en/server/configuration/cluster) `WK_NODE_ID` | Identifies this node as the stable identity used by messages and cluster state. | Required at startup, non-zero, and unique within the cluster; do not change it after data exists. |
| [`node.data_dir`](/en/server/configuration/storage) `WK_NODE_DATA_DIR` | Stores this node's durable Controller, Slot, Channel, and related data. | Required at startup; the directory must be writable and should reside on persistent, backed-up storage. |
| [`cluster.listen_addr`](/en/server/configuration/cluster) `WK_CLUSTER_LISTEN_ADDR` | Listens for Cluster RPC between nodes. | Required at startup; it may bind `0.0.0.0`, but the address given to peers must be reachable and must not be a wildcard. |
| [`cluster.nodes`](/en/server/configuration/cluster) `WK_CLUSTER_NODES` | Defines the static Controller voter list, with each entry containing a node `id` and cluster `addr`. | When `cluster.seeds` is also unset, omission or an empty list falls back to an implicit single-node cluster; it cannot be combined with non-empty `cluster.seeds`. The environment value must be JSON and replaces the whole list. |
| [`cluster.slot_replica_n`](/en/server/configuration/cluster) `WK_CLUSTER_SLOT_REPLICA_N` | Sets the voter replicas for each Slot Raft Group when the cluster is first initialized. | `0` derives the static voter count, or uses `1` during seed joining; existing clusters follow persisted Controller state. |
| [`gateway.listeners`](/en/server/configuration/networking) `WK_GATEWAY_LISTENERS` | Defines the TCP or WebSocket listeners used by clients. | Omission opens WKProto TCP on `0.0.0.0:5100` and WSMux on `0.0.0.0:5200`; an explicit empty list disables Gateway. The environment value must be JSON. |
| [`gateway.token_auth_on`](/en/server/configuration/security) `WK_GATEWAY_TOKEN_AUTH_ON` | Requires a CONNECT token to exactly match the stored device token for the same UID and `device_flag`. | Defaults to `true`; an empty token, missing record, or mismatch returns `ReasonAuthFail`. Disable only temporarily during a controlled compatibility migration. |
| [`api.listen_addr`](/en/server/configuration/networking) `WK_API_LISTEN_ADDR` | Listens for the WuKongIM HTTP API, including health and switch-controlled metrics, debug, and related HTTP routes. | Omission or an empty value disables this HTTP service; it does not create client Gateway or Manager listeners. |
| [`manager.listen_addr`](/en/server/configuration/security) `WK_MANAGER_LISTEN_ADDR` | Listens for the separate Manager administration service. | Omission or an empty value disables Manager; bind loopback or a management address, use SSH tunneling or a private HTTPS proxy, and enable authentication. |
| [`manager.auth_on`](/en/server/configuration/security) `WK_MANAGER_AUTH_ON` | Requires JWT login authentication for Manager routes. | Omission is `false`; when `true`, both `manager.jwt_secret` and `manager.users` are required. |
| [`log.level`](/en/server/configuration/observability) `WK_LOG_LEVEL` | Sets the minimum application log level that is recorded. | Defaults to `info`; allowed values are `debug`, `info`, `warn`, and `error`. Avoid leaving `debug` enabled after production troubleshooting. |
## Continue reading [#continue-reading]
* See the [Configuration overview](/en/server/configuration) for file lookup, environment overrides, and list replacement.
* See the [Configuration Reference](/en/server/configuration/reference) for seed joining, channel replicas, metrics, webhooks, and every other setting.
# Configuration (/en/server/configuration)
The configuration file is `wukongim.toml`. Start with [Common Configurations](/en/server/configuration/common-configurations) to choose the fields for your task.
## Select a configuration file [#select-a-configuration-file]
Use `-config` to select an explicit path. Otherwise, search in order and **stop at the first match**:
1. `./wukongim.toml`
2. `./conf/wukongim.toml`
3. `/etc/wukongim/wukongim.toml`
Copy the example, start, and inspect lookup rules
WuKongIM uses TOML as its primary configuration format. Start by copying `wukongim.toml.example` from the repository root, and keep the example file as the commented reference.
An explicit path is the easiest option to reproduce:
```bash
cp wukongim.toml.example wukongim.toml
GOWORK=off go run ./cmd/wukongim -config ./wukongim.toml
```
Without `-config`, the server searches these locations in order and stops at the first match:
1. `./wukongim.toml`
2. `./conf/wukongim.toml`
3. `/etc/wukongim/wukongim.toml`
## Environment overrides [#environment-overrides]
Use uppercase underscore names with the `WK_` prefix. Scalars override TOML; valid JSON lists **replace the whole list, never append**. Audit both sources and the effective value.
Complete scalar and list override commands
Environment variables use the `WK_` prefix with uppercase underscore-separated names and override the matching file values. For example:
```bash
WK_API_LISTEN_ADDR=127.0.0.1:6001 \
WK_LOG_LEVEL=debug \
GOWORK=off go run ./cmd/wukongim -config ./wukongim.toml
```
List values must use JSON to replace the complete list; they cannot append one item:
```bash
WK_CLUSTER_NODES='[{"id":1,"addr":"127.0.0.1:7001"}]' \
GOWORK=off go run ./cmd/wukongim -config ./wukongim.toml
```
An environment-supplied list replaces the whole list. Deployment tooling should write the complete target list as one valid JSON string.
## Configuration areas used by the quick start [#configuration-areas-used-by-the-quick-start]
All TOML sections and their responsibilities
| TOML section | Responsibility |
| -------------------------- | ------------------------------------------------------------------------------ |
| `[node]` | Node ID and data directory |
| `[cluster]` | Cluster ID, node addresses, Slots, replication, and inter-node listener |
| `[api]` | HTTP API and external TCP/WebSocket addresses |
| `[manager]` | Manager listener, authentication, JWT, and administrative users |
| `[gateway]` | TCP/WebSocket listeners, CONNECT token authentication, and asynchronous queues |
| `[observability]`, `[log]` | Metrics switches and logs |
| `[diagnostics]` | Diagnostic sampling, buffering, and slow-request thresholds |
`gateway.token_auth_on` defaults to `true`. A trusted backend first stores the token for a UID and device category through `/user/token`; later CONNECT requests must carry the exact value. Do not disable it in production merely to simplify integration. See [Security & Access](/en/server/configuration/security) for the full boundary.
Restart the node after changing configuration. Node IDs and advertised addresses must be unique in a multi-node cluster. `0.0.0.0` is a listen address only and must not be advertised for other nodes to connect to.
## Local security check [#local-security-check]
Examples are for development. Replace credentials, configure TLS, restrict management and diagnostics, use independent persistent disks, and validate real workloads. Wrong, missing, and revoked tokens must fail CONNECT.
**Done:** run `wukongim config validate --config `, then restart using your deployment method. Admit each node only after `/readyz` returns `200` with `ready: true`, then verify business behavior.
Full pre-production checks and domain references
The example configuration is for development only. Before production use, at minimum:
* Replace Manager accounts, JWT secrets, cluster join tokens, and other fixed capability credentials.
* Keep `gateway.token_auth_on=true`, and prove that wrong, missing, and revoked tokens cannot CONNECT.
* Restrict Manager, metrics, debug, benchmark, and diagnostic surfaces to trusted networks.
* Configure TLS and access policies for client and administrative traffic.
* Put each node's data on independent, durable, monitored storage.
* Tune queues, concurrency, retention, and capacity against real traffic, group sizes, and online-user counts.
If you are unsure which fields to change first, start with [Common Configurations](/en/server/configuration/common-configurations). Then continue through [Nodes & Cluster](/en/server/configuration/cluster), [Networking & Client Access](/en/server/configuration/networking), [Messages & Storage](/en/server/configuration/storage), [Security & Access](/en/server/configuration/security), and [Logs & Observability](/en/server/configuration/observability). See [Configuration Reference](/en/server/configuration/reference) for every public TOML and environment mapping.
# Networking & Client Access (/en/server/configuration/networking)
**Listen address: where this node accepts connections. Published address: where others connect.** Match domains, ports, and TLS to the actual network path.
This example terminates WSS at a TLS proxy and forwards WS upstream. Route the external `/ws` path correctly. The trusted backend accesses the WuKongIM HTTP API to obtain client routing.
## Traffic planes [#traffic-planes]
| Who connects | Entry to use |
| --------------------- | ---------------------------------------------------------------- |
| Clients | Controlled TCP / WS / WSS edge; reachable from client networks |
| Business backend | WuKongIM HTTP API on a trusted network or behind external auth |
| Peer nodes | Private Transport with bidirectional reachability |
| Admins and collectors | SSH / private Manager access; metrics and diagnostics restricted |
Complete traffic, field, and production-boundary reference
| Traffic | Primary configuration | Consumer | Production boundary |
| -------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Inter-node Transport | `cluster.listen_addr`, static `nodes[].addr`, or `cluster.advertise_addr` | Other WuKongIM nodes | Restrict to the cluster network and require bidirectional reachability |
| WuKongIM HTTP API | `api.listen_addr` | Business backends, health checks, observability collectors | Place on a trusted network or behind external authentication |
| Client Gateway | `gateway.listeners` | TCP and WebSocket clients | Expose through a controlled edge and enforce connection capacity |
| Client discovery | `api.external_tcp_addr`, `external_ws_addr`, `external_wss_addr` | Callers of `/route` | Reachable from every target client network |
| Manager | `manager.listen_addr` | Administrators and automation | Loopback or management network only; use SSH tunneling or a private HTTPS proxy and keep authentication enabled |
| Prometheus | `prometheus.listen_addr` or an external service | Manager and collectors | Never expose directly to the Internet |
## Listen is not advertise [#listen-is-not-advertise]
| Address purpose | Value in this example |
| ------------------ | ------------------------------------------------------ |
| Node WS listener | `0.0.0.0:5200` |
| Client WSS address | `wss://im.example.com/ws` |
| Peer address | Real private host and port; never wildcard or loopback |
Static peers use `nodes[].addr`; seed joining uses `cluster.advertise_addr`. Client fields `api.external_*` may point to a proxy; configure its reachable ports and TLS separately.
Complete TOML example and TLS forwarding notes
`0.0.0.0` and `[::]` are useful wildcard listeners, but they are not routable peer or client destinations. Static clusters use `nodes[].addr` for peer connections; seed joining uses `cluster.advertise_addr`. Those values need stable, resolvable, reachable hosts and ports.
Client-advertised fields describe the address clients ultimately use, which may be a load balancer or edge proxy:
```toml
[api]
listen_addr = "0.0.0.0:5001"
external_tcp_addr = "im.example.com:5100"
external_wss_addr = "wss://im.example.com/ws"
[gateway]
listeners = [
{ name = "tcp", network = "tcp", address = "0.0.0.0:5100", transport = "gnet", protocol = "wkproto" },
{ name = "ws", network = "websocket", address = "0.0.0.0:5200", transport = "gnet", protocol = "wsmux" }
]
```
The names and ports are structural examples. If a load balancer, reverse proxy, or service mesh terminates TLS, still verify the external WSS address, certificate, host and path forwarding, and upstream protocol together.
## Exposure policy [#exposure-policy]
WuKongIM HTTP API routes do not validate general business identity. Use a trusted network or external auth. Public ingress contains only required client ports and a protected API. Keep Manager auth on; use loopback, private networking, or SSH.
Restrict Transport, metrics, debug, benchmark, and diagnostics separately. A shared HTTP listener does not imply equal trust.
Full exposure policy and health-check distinction
* Public ingress should contain only required client ports and a protected WuKongIM HTTP API.
* Give node Transport, Manager, `/metrics`, `/top/v1/*`, `/debug/pprof/*`, Benchmark, and diagnostics separate network policies. Sharing an HTTP listener does not give them the same trust level.
* WuKongIM HTTP API routes do not provide general business identity validation. Establish that boundary with a trusted network, API gateway, or external authentication proxy.
* Load balancers must use `/readyz` for traffic admission. `/healthz` only proves process liveness and cannot prove the node is ready to serve.
## Validate before publishing [#validate-before-publishing]
| Check | Required result |
| --------------------------- | -------------------------------------------------------------------- |
| Peer network | Every peer-advertised address is reachable both ways |
| Backend and client networks | Backend reaches the API; clients reach every `/route` result |
| Handshake and proxy | TCP / WS / WSS, TLS chain, host / path, and idle timeouts work |
| Unauthorized networks | Manager, metrics, debug, benchmark, and diagnostics stay unreachable |
| Each node | `/readyz` returns `200` with `ready: true` before pool admission |
`/healthz` proves only process liveness; it cannot replace readiness checks.
Full validation steps before publishing
1. Test bidirectional Transport connections from each node to every peer-advertised address.
2. Reach the HTTP API from the business network and every `/route` result from the target client network.
3. Verify TCP, WS/WSS handshakes, the TLS chain, timeouts, and proxy idle-connection policy.
4. Confirm Manager, metrics, debug, benchmark, and diagnostic endpoints are unreachable from unauthorized networks.
5. Check `/readyz` per node and admit only nodes returning `200` with `ready: true`.
Once paths are explicit, use [Security & Access](/en/server/configuration/security) to constrain credentials and administrative surfaces.
# Logs & Observability (/en/server/configuration/observability)
Establish node, time, and resource baselines first; add diagnostics with expiry and capture limits when needed.
## Capability layers [#capability-layers]
### Baseline monitoring [#baseline-monitoring]
Collect continuously with bounded retention; expose metrics only to collector networks.
Use administrative networks and raise cost gradually. pprof is off by default.
**Acceptance:** correlate logs and metrics by node and time. Collection success does not prove readiness; traffic admission still checks every node's `/readyz` for `200` with `ready: true`.
Complete capabilities, sections, and exposure boundaries
Build a baseline that can answer which node, when, and which resource or queue changed before enabling expensive diagnostics. Observability consumes CPU, memory, disk, and network, and may expose topology or request information.
| Capability | Section | Purpose | Exposure boundary |
| ------------------- | -------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------- |
| Logs | `[log]` | Level, format, console, and rolling files | Central collection with bounded retention and reads |
| Prometheus metrics | `[observability]` | Controls whether `/metrics` is available | Collector network only, never public Internet |
| Prometheus service | `[prometheus]` | Optional app-managed child process or an external query address | Plan ownership and capacity separately from the node |
| Top | `[top]` | Real-time node snapshots with bounded history | Administrative network and restricted callers |
| Diagnostic sampling | `[diagnostics]` | Buffer, normal/error/deep sampling, and slow thresholds | Raise gradually with cost and expiry controls |
| Debug/pprof | `observability.debug_api_enable` | Expensive process diagnostics | Off by default and temporarily exposed only to authorized operators |
## Logs [#logs]
Use parseable logs with node identity and time, collected centrally. Bound disk use with `log.max_size`, `max_age`, `max_backups`, and `compress`, and alert on log storage.
Before temporary `debug` logging, assess volume, sensitive data, and performance. Restore the level afterward; record who changed it, the time window, and incident.
Complete log retention and diagnostic records
Production logs should use a parseable format, carry node identity and time, and be collected centrally. `log.max_size`, `max_age`, `max_backups`, and `compress` bound local use together. Alert on the log directory anyway so logs cannot crowd out message data.
Before raising the level to `debug`, evaluate write volume, sensitive data, and performance cost. Restore the original level afterward and retain who changed it, the time window, and the related incident.
## Metrics and Prometheus [#metrics-and-prometheus]
Set `observability.metrics_enable=true` for node metrics and keep `observability.debug_api_enable=false`.
* **Existing Prometheus:** `prometheus.enable=false`; Manager queries the external service through `prometheus.query_base_url`.
* **App-managed:** `prometheus.enable=true` starts a child process. Define ownership, TSDB, retention, scrape interval, and availability.
Example `127.0.0.1:9099` is for development; it does not define production listening or retention policy.
Complete metrics TOML and Prometheus ownership
Enable node metrics with:
```toml
[observability]
metrics_enable = true
debug_api_enable = false
```
`prometheus.enable = true` starts an app-managed Prometheus child process. If an external Prometheus already exists, keep it `false` and use `prometheus.query_base_url` for Manager queries. Do not treat both modes as the same ownership model.
The repository example reserves `127.0.0.1:9099` for local app-managed Prometheus to avoid common port conflicts. It is a development example, not a production listener or retention policy. The observability owner must define production scrape targets, TSDB directory, time and size retention, interval, and availability.
Load balancers still use `/readyz` for admission. Metrics and logs explain why a node is not ready; they do not replace the readiness gate.
## Top, diagnostics, and Debug [#top-diagnostics-and-debug]
Start with Top and low-cost sampling. Bound intervals, history, buffers, and sampling rates. At high traffic and with 100,000-member groups, excessive sampling raises CPU, memory, and contention.
pprof requires `debug_api_enable=true`: authorize operators, restrict sources, record start/end times, bound captures, and disable it afterward. Treat artifacts as internal sensitive data even after redaction.
Complete Top, sampling, and Debug costs
Top collection interval and history window create sampling and memory cost. Start diagnostic `buffer_size`, rates, slow thresholds, deep sampling, and debug matches at low cost. At high traffic and with 100,000-member groups, excessive sampling can amplify CPU, memory, and contention.
pprof is available only with `debug_api_enable = true`. During temporary access, restrict sources, record start and end times, bound captures, and disable it afterward. Treat diagnostic artifacts as internal sensitive data even after configured fields are redacted.
## Minimum production evidence [#minimum-production-evidence]
* Establish per-node resource, request-latency, queue, and error baselines.
* Assign owners and alerts for readiness failure, disk pressure, sustained backlog, and critical errors.
* Collector failure cannot block messaging. Load-test retention and sampling costs; one peak does not establish sustained capacity.
Complete production evidence and alert checklist
* Logs, metrics, and identity labels correlate for every node.
* CPU, memory, file descriptors, goroutines, disk, network, request latency, queues, and errors have baselines.
* `/readyz` failure, disk pressure, sustained queue backlog, and critical errors have alerts and owners.
* Collector failure cannot block the message path.
* Retention and sampling costs are load-tested; one peak result is not sustainable capacity evidence.
Continue with [Health & Monitoring](/en/server/operations/health-and-monitoring) to establish alert boundaries.
# Configuration Reference (/en/server/configuration/reference)
This is the quick-reference handbook for WuKongIM's public startup configuration, checked against the startup schema. Every row explains what the field controls, its one environment mapping, and any important default, zero-value, mutual-exclusion, compatibility, or security boundary.
## How to use this reference [#how-to-use-this-reference]
* Browse by domain with the page outline, or use browser Find to search for a TOML key, `WK_*` environment variable, or keyword.
* Prefer TOML for normal configuration; environment variables override TOML. Unknown TOML paths and unknown `WK_*` variables fail startup.
* `duration` uses Go syntax such as `500ms`, `5s`, or `24h`. Environment values for `string_list` and `object_list` must be JSON and replace the complete list.
* Configuration is loaded at node startup. Restart the node after a change unless the subsystem explicitly documents otherwise.
Starting with `v3.0.0-beta.7`, `wukongim init` creates a security-oriented installation baseline at `/etc/wukongim/wukongim.toml` by default and explicitly disables Diagnostics and Plugin. Use `wukongim init --config PATH` to choose another location; the original `wukongim config init --config PATH` compatibility entry point also remains available. `wukongim.toml.example` is a loadable development and tuning baseline. Neither baseline is a complete catalog of runtime defaults. In the table, “omitted” means neither TOML nor an environment value was supplied; it may differ from an explicit `0`, empty string, or `false`.
`node.id`, `node.data_dir`, and `cluster.listen_addr` are always required. Seed joining additionally requires `cluster.id`, non-empty `cluster.seeds`, a peer-reachable `cluster.advertise_addr`, and a non-empty `cluster.join_token`.
For task-oriented field discovery, start with [Common Configurations](/en/server/configuration/common-configurations); this page is the field-by-field reference.
## Node [#node]
| TOML | Environment | Type | Description |
| --------------- | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------ |
| `node.id` | `WK_NODE_ID` | `uint64` | Stable, non-zero node ID unique within the cluster; do not change it after data exists. Required at startup. |
| `node.data_dir` | `WK_NODE_DATA_DIR` | `string` | Root directory for this node's durable data; place it on writable, backed-up storage. Required at startup. |
## Cluster [#cluster]
### Identity and discovery [#identity-and-discovery]
| TOML | Environment | Type | Description |
| ------------------------ | --------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cluster.listen_addr` | `WK_CLUSTER_LISTEN_ADDR` | `string` | Listener for inter-node Cluster RPC; it may bind `0.0.0.0`, but a wildcard must not be advertised to peers. Required at startup. |
| `cluster.start_timeout` | `WK_CLUSTER_START_TIMEOUT` | `duration` | Maximum wait for startup readiness gates, including committed Slot write probes; omitted or `0` uses `30s`. Must be non-negative. Size from measured cold recovery; quorum, routing, and placement checks and steady-state request timeouts remain unchanged. |
| `cluster.id` | `WK_CLUSTER_ID` | `string` | Stable Controller cluster identity; required for seed joining. Static inventories and implicit single-node clusters derive it from node IDs when omitted. |
| `cluster.seeds` | `WK_CLUSTER_SEEDS` | `string_list` | Existing node addresses used to discover the cluster during dynamic joining; must be non-empty and cannot be combined with `cluster.nodes`. |
| `cluster.advertise_addr` | `WK_CLUSTER_ADVERTISE_ADDR` | `string` | Stable address stored in membership for peers to call this node; required with `cluster.seeds` and must be reachable by peers. |
| `cluster.join_token` | `WK_CLUSTER_JOIN_TOKEN` | `string` | Shared credential that authenticates dynamic cluster joining; it must be non-empty whenever present and is required with `cluster.seeds`. Redacted in startup snapshots and diagnostics. |
| `cluster.nodes` | `WK_CLUSTER_NODES` | `object_list` | Static Controller voters with `{id, addr}` elements; a non-empty list must contain the current `node.id`, with non-zero unique IDs and non-empty addresses. It cannot be combined with `cluster.seeds`; an explicit empty list (including environment JSON `null`) currently falls back to an implicit single-node cluster and cannot disable clustering. |
### Slot Raft [#slot-raft]
| TOML | Environment | Type | Description |
| --------------------------------------------- | ------------------------------------------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cluster.initial_slot_count` | `WK_CLUSTER_INITIAL_SLOT_COUNT` | `uint32` | Independent Slot Raft Groups written to persisted Controller state at first initialization; it cannot exceed `cluster.hash_slot_count`, and omitted or `0` uses `1`. Existing clusters follow the persisted value: changing this setting does not resize Slots, and increasing it may block readiness. Do not change it after initialization. |
| `cluster.hash_slot_count` | `WK_CLUSTER_HASH_SLOT_COUNT` | `uint16` | Stable hash-slot partitions that route keys across Slot Raft Groups; omitted or `0` uses `256`. Do not change it after cluster initialization. |
| `cluster.slot_replica_n` | `WK_CLUSTER_SLOT_REPLICA_N` | `uint16` | Voter replicas per Slot Raft Group when initializing Controller state; existing clusters follow persisted Controller state. `0` derives the static voter count but falls back to `1` during seed joining; an explicit static value cannot exceed the voter count. |
| `cluster.channel_replica_n` | `WK_CLUSTER_CHANNEL_REPLICA_N` | `uint16` | Desired data replicas for newly created channels; `0` follows the local effective `cluster.slot_replica_n`, becoming `1` during seed joining rather than reading the persisted Controller replica count. Multi-replica clusters should set the same explicit value on every node and fit available nodes and failure domains. |
| `cluster.slot_tick_interval` | `WK_CLUSTER_SLOT_TICK_INTERVAL` | `duration` | Local Slot Raft tick interval; defaults to `50ms` and must be greater than `0`. |
| `cluster.slot_election_tick` | `WK_CLUSTER_SLOT_ELECTION_TICK` | `int` | Ticks waited before a Slot Raft election can start; defaults to `40` and must exceed the heartbeat tick. |
| `cluster.slot_heartbeat_tick` | `WK_CLUSTER_SLOT_HEARTBEAT_TICK` | `int` | Slot Raft heartbeat interval in ticks; defaults to `2` and must be greater than `0`. |
| `cluster.slot_log_compaction_enabled` | `WK_CLUSTER_SLOT_LOG_COMPACTION_ENABLED` | `bool` | Enables local Slot Raft snapshots and log compaction; enabled when omitted. |
| `cluster.slot_log_compaction_trigger_entries` | `WK_CLUSTER_SLOT_LOG_COMPACTION_TRIGGER_ENTRIES` | `uint64` | Applied entries since the last snapshot required before another compaction; defaults to `10000` and must be positive. |
| `cluster.slot_log_compaction_check_interval` | `WK_CLUSTER_SLOT_LOG_COMPACTION_CHECK_INTERVAL` | `duration` | Minimum interval between Slot Raft log-compaction checks; defaults to `30s` and must be positive. |
### Channel Runtime [#channel-runtime]
| TOML | Environment | Type | Description |
| -------------------------------------------------- | ----------------------------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------- |
| `cluster.channel_reactor_count` | `WK_CLUSTER_CHANNEL_REACTOR_COUNT` | `int` | Channel Reactor partitions on this node; `0` derives a CPU-aware runtime value. |
| `cluster.channel_store_append_workers` | `WK_CLUSTER_CHANNEL_STORE_APPEND_WORKERS` | `int` | Maximum blocking Leader store-append workers; `0` keeps the Channel runtime default. |
| `cluster.channel_store_append_batch_max_wait` | `WK_CLUSTER_CHANNEL_STORE_APPEND_BATCH_MAX_WAIT` | `duration` | Maximum wait for coalescing Leader store-appends across channels; `0` keeps the Channel worker default. |
| `cluster.channel_store_apply_workers` | `WK_CLUSTER_CHANNEL_STORE_APPLY_WORKERS` | `int` | Maximum blocking Follower store-apply workers; `0` keeps the Channel runtime default. |
| `cluster.channel_rpc_workers` | `WK_CLUSTER_CHANNEL_RPC_WORKERS` | `int` | Maximum blocking Channel replication RPC workers; `0` uses `96`. |
| `cluster.channel_rpc_batch_max_items` | `WK_CLUSTER_CHANNEL_RPC_BATCH_MAX_ITEMS` | `int` | Maximum same-target Pull or PullHint items coalesced into one RPC; `0` uses `8`. |
| `cluster.max_channels` | `WK_CLUSTER_MAX_CHANNELS` | `int` | Maximum loaded Channel Runtimes on this node; `0` leaves the count unlimited. |
| `cluster.channel_append_batch_max_records` | `WK_CLUSTER_CHANNEL_APPEND_BATCH_MAX_RECORDS` | `int` | Queued record count that triggers a Channel store-append flush; `0` keeps the runtime default. |
| `cluster.channel_append_batch_max_wait` | `WK_CLUSTER_CHANNEL_APPEND_BATCH_MAX_WAIT` | `duration` | Maximum age of the oldest queued Channel append before flushing; `0` keeps the runtime default. |
| `cluster.channel_append_batch_adaptive_flush` | `WK_CLUSTER_CHANNEL_APPEND_BATCH_ADAPTIVE_FLUSH` | `bool` | Enables a shorter adaptive flush delay for low-traffic channels; disabled when omitted. |
| `cluster.channel_append_batch_cold_max_wait` | `WK_CLUSTER_CHANNEL_APPEND_BATCH_COLD_MAX_WAIT` | `duration` | Low-traffic channel delay used by adaptive flushing; `0` uses the normal batching window. |
| `cluster.channel_follower_recovery_probe_interval` | `WK_CLUSTER_CHANNEL_FOLLOWER_RECOVERY_PROBE_INTERVAL` | `duration` | Base interval for probing parked Followers for recovery; `0` keeps the Channel runtime default. |
| `cluster.channel_follower_recovery_probe_jitter` | `WK_CLUSTER_CHANNEL_FOLLOWER_RECOVERY_PROBE_JITTER` | `duration` | Random jitter window that spreads parked-Follower recovery probes; `0` keeps the runtime default. |
### Health and durable commits [#health-and-durable-commits]
| TOML | Environment | Type | Description |
| ----------------------------------------- | -------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cluster.node_health_report_interval` | `WK_CLUSTER_NODE_HEALTH_REPORT_INTERVAL` | `duration` | Interval for reporting compact node health to Controller; defaults to `5s` and must be positive. |
| `cluster.node_health_report_ttl` | `WK_CLUSTER_NODE_HEALTH_REPORT_TTL` | `duration` | How long Controller trusts the latest node-health report; defaults to `30s` and cannot be shorter than the report interval. |
| `cluster.commit_coordinator_sync` | `WK_CLUSTER_COMMIT_COORDINATOR_SYNC` | `bool` | Durable-commit compatibility switch; omitted is equivalent to `true`, and an explicit value must also be `true` because WuKongIM does not allow durable sync to be disabled. |
| `cluster.commit_coordinator_flush_window` | `WK_CLUSTER_COMMIT_COORDINATOR_FLUSH_WINDOW` | `duration` | Maximum time to wait for additional Channel durable commit requests. After expiry, one bounded snapshot of already queued requests may join the same commit, subject to the batch limits. Defaults to `500us`; an explicit value must be positive. |
| `cluster.commit_coordinator_max_requests` | `WK_CLUSTER_COMMIT_COORDINATOR_MAX_REQUESTS` | `int` | Maximum logical requests grouped into one physical commit; `0` applies no request-count limit. |
| `cluster.commit_coordinator_max_records` | `WK_CLUSTER_COMMIT_COORDINATOR_MAX_RECORDS` | `int` | Maximum message records grouped into one physical commit; `0` applies no record-count limit. |
| `cluster.commit_coordinator_max_bytes` | `WK_CLUSTER_COMMIT_COORDINATOR_MAX_BYTES` | `int` | Maximum approximate payload bytes grouped into one physical commit; `0` applies no byte limit. |
| `cluster.commit_coordinator_shards` | `WK_CLUSTER_COMMIT_COORDINATOR_SHARDS` | `int` | Independent commit coordinators for the message database; `0` uses `1`. Increase only after storage-specific load tests. |
| `cluster.storage_disk_slow_threshold` | `WK_CLUSTER_STORAGE_DISK_SLOW_THRESHOLD` | `duration` | Message database disk operations slower than this are counted in slow-disk metrics (`wukongim_storage_pebble_disk_slow_*`); defaults to `1s`, and an explicit value must be positive. Observation only; it does not change durability. |
## Channel Migration [#channel-migration]
| TOML | Environment | Type | Description |
| -------------------------------------- | ----------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `channel_migration.enable` | `WK_CHANNEL_MIGRATION_ENABLE` | `bool` | Starts the background worker that advances Channel migrations and creates repair tasks; enabled when omitted. |
| `channel_migration.scan_interval` | `WK_CHANNEL_MIGRATION_SCAN_INTERVAL` | `duration` | Interval for scanning and advancing Channel migration work; omitted uses `1s`, and an explicit value must be positive. |
| `channel_migration.scan_limit` | `WK_CHANNEL_MIGRATION_SCAN_LIMIT` | `int` | Channel Runtime metadata rows read from one Slot page per scan; omitted uses `64`, and an explicit value must be positive. |
| `channel_migration.max_pages_per_tick` | `WK_CHANNEL_MIGRATION_MAX_PAGES_PER_TICK` | `int` | Maximum physical Slot pages scanned per worker tick; omitted uses `1`, and an explicit value must be positive. |
| `channel_migration.max_tasks_per_tick` | `WK_CHANNEL_MIGRATION_MAX_TASKS_PER_TICK` | `int` | Maximum repair tasks created per scan; omitted uses `1`, and an explicit value must be positive. |
| `channel_migration.task_limit` | `WK_CHANNEL_MIGRATION_TASK_LIMIT` | `int` | Maximum active migration tasks inspected by the executor per tick; omitted uses `1`, and an explicit value must be positive. |
## Channel & Retention [#channel--retention]
| TOML | Environment | Type | Description |
| ---------------------------------------------- | ------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------- |
| `channel.message_retention_physical_gc_enable` | `WK_CHANNEL_MESSAGE_RETENTION_PHYSICAL_GC_ENABLE` | `bool` | Enables background physical deletion of local messages beyond each channel's retention boundary; disabled when omitted. |
| `channel.message_retention_scan_interval` | `WK_CHANNEL_MESSAGE_RETENTION_SCAN_INTERVAL` | `duration` | Interval for scanning one channel-catalog page for retention cleanup; `0` uses `1m`. |
| `channel.message_retention_channel_batch_size` | `WK_CHANNEL_MESSAGE_RETENTION_CHANNEL_BATCH_SIZE` | `int` | Maximum local channel-catalog entries processed per cleanup pass; `0` uses `128`. |
| `channel.message_retention_max_trim_messages` | `WK_CHANNEL_MESSAGE_RETENTION_MAX_TRIM_MESSAGES` | `int` | Maximum message rows deleted for one channel per cleanup attempt; `0` uses `1000`. |
| `channel.message_retention_max_trim_bytes` | `WK_CHANNEL_MESSAGE_RETENTION_MAX_TRIM_BYTES` | `int` | Maximum payload bytes deleted for one channel per cleanup attempt; `0` applies no byte limit. |
| `channel.large_group_subscriber_threshold` | `WK_CHANNEL_LARGE_GROUP_SUBSCRIBER_THRESHOLD` | `int` | Treats a channel as a large group when ordinary subscribers exceed this count; defaults to `500` and must be positive. |
## API & Client Addresses [#api--client-addresses]
| TOML | Environment | Type | Description |
| ----------------------- | --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `api.listen_addr` | `WK_API_LISTEN_ADDR` | `string` | Listener for the WuKongIM HTTP API; an empty value disables this HTTP service. |
| `api.external_tcp_addr` | `WK_EXTERNAL_TCPADDR` | `string` | WKProto TCP address override published to capacity discovery and similar callers, in `host:port` form; empty derives from the first matching `gateway.listeners` entry and stays empty if none matches. It does not create a listener. |
| `api.external_ws_addr` | `WK_EXTERNAL_WSADDR` | `string` | WebSocket URL override published to capacity discovery and similar callers; empty derives from the first matching `gateway.listeners` entry and stays empty if none matches. It does not create a listener and is redacted from diagnostic artifacts. |
| `api.external_wss_addr` | `WK_EXTERNAL_WSSADDR` | `string` | Secure WebSocket URL override published to capacity discovery and similar callers; empty derives from the first matching `gateway.listeners` entry and stays empty if none matches. It does not create a listener and is redacted from diagnostic artifacts. |
When the corresponding `api.external_*` is omitted, default external `/route` and `/route/batch` complete only wildcard listener hosts from a valid request Host, preserving the Gateway port, scheme, and path. Explicit addresses, concrete listener hosts (including loopback), intranet queries, and explicit node selectors stay unchanged; capacity discovery still returns listener-derived addresses. Completed responses use `Cache-Control: no-store`. Completion ignores forwarded headers and cannot infer NAT ports or TLS ingress; reverse proxies should preserve the original Host. Configure client-reachable overrides for remapped external ports, separate ingress, or a backend calling `/route` through an internal host.
## Manager [#manager]
| TOML | Environment | Type | Description |
| --------------------- | ------------------------ | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `manager.listen_addr` | `WK_MANAGER_LISTEN_ADDR` | `string` | Listener for the Manager administration service; an empty value disables Manager. |
| `manager.auth_on` | `WK_MANAGER_AUTH_ON` | `bool` | Requires JWT login authentication for Manager routes; disabled when omitted. With `false`, Manager routes require no JWT, so do not expose an unauthenticated service to untrusted networks. |
| `manager.jwt_secret` | `WK_MANAGER_JWT_SECRET` | `string` | Secret used to sign and verify Manager JWTs; required when Manager is listening with authentication enabled. Redacted in startup snapshots and diagnostics. |
| `manager.jwt_issuer` | `WK_MANAGER_JWT_ISSUER` | `string` | Issuer written to the Manager JWT `iss` claim. |
| `manager.jwt_expire` | `WK_MANAGER_JWT_EXPIRE` | `duration` | Manager JWT lifetime; when Manager is listening with authentication enabled, omitted or explicit `0` uses `24h`, while a negative value fails startup. |
| `manager.users` | `WK_MANAGER_USERS` | `object_list` | Static Manager users with `username`, `password`, and `permissions[{resource, actions}]`; actions are `r`, `w`, or `*`. Required when Manager is listening with authentication enabled and fully redacted. |
## Benchmark [#benchmark]
| TOML | Environment | Type | Description |
| ----------------------------- | -------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bench.api_enable` | `WK_BENCH_API_ENABLE` | `bool` | Exposes `/bench/v1/*` routes on the API listener for controlled benchmark environments; disabled when omitted and requires non-empty `api.listen_addr`. |
| `bench.api_token` | `WK_BENCH_API_TOKEN` | `string` | Bench API bearer token; an empty value skips token validation, so set it whenever the API is remotely reachable. Redacted in snapshots and diagnostics. |
| `bench.api_max_batch_size` | `WK_BENCH_API_MAX_BATCH_SIZE` | `int` | Maximum top-level records accepted by one Bench API mutation request; omitted uses `10000`, while an explicit value at or below `0` removes this limit and is not recommended for remotely reachable environments. |
| `bench.api_max_payload_bytes` | `WK_BENCH_API_MAX_PAYLOAD_BYTES` | `int` | Maximum JSON body bytes accepted by one Bench API mutation request; omitted uses `10485760` (10 MiB), while an explicit value at or below `0` removes this limit and is not recommended for remotely reachable environments. |
## Observability Switches [#observability-switches]
| TOML | Environment | Type | Description |
| -------------------------------- | --------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `observability.metrics_enable` | `WK_METRICS_ENABLE` | `bool` | Enables runtime metric observers and exposes `/metrics` when an API listener exists; disabled when omitted. The HTTP metrics endpoint requires non-empty `api.listen_addr`. |
| `observability.debug_api_enable` | `WK_DEBUG_API_ENABLE` | `bool` | Exposes `/debug` diagnostics on the API listener; disabled when omitted and requires non-empty `api.listen_addr`. Never publish it directly to the internet. |
## Prometheus [#prometheus]
| TOML | Environment | Type | Description |
| ---------------------------- | ------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prometheus.enable` | `WK_PROMETHEUS_ENABLE` | `bool` | Starts a Prometheus child process managed by WuKongIM; disabled when omitted. Enabling also requires an API listener, metrics, and valid scrape targets; keep it disabled with external Prometheus. |
| `prometheus.query_base_url` | `WK_PROMETHEUS_QUERY_BASE_URL` | `string` | Base URL used by Manager to query an external Prometheus HTTP API; it must be an HTTP(S) URL with a host and no query or fragment. Empty may fall back to managed Prometheus. Redacted from diagnostics. |
| `prometheus.binary_path` | `WK_PROMETHEUS_BINARY_PATH` | `string` | External Prometheus executable used by the managed process; empty uses the embedded binary. |
| `prometheus.listen_addr` | `WK_PROMETHEUS_LISTEN_ADDR` | `string` | Web listener for the managed Prometheus process, in `host:port` form; defaults to `127.0.0.1:9099`. |
| `prometheus.data_dir` | `WK_PROMETHEUS_DATA_DIR` | `string` | Directory for generated Prometheus configuration and TSDB data; empty derives it from `node.data_dir`. |
| `prometheus.retention_time` | `WK_PROMETHEUS_RETENTION_TIME` | `duration` | Time-based retention window for the managed Prometheus TSDB; omitted or `0` uses `360h`. |
| `prometheus.retention_size` | `WK_PROMETHEUS_RETENTION_SIZE` | `string` | Optional size-based retention limit for the managed Prometheus TSDB; empty applies no size limit. |
| `prometheus.scrape_interval` | `WK_PROMETHEUS_SCRAPE_INTERVAL` | `duration` | Interval for managed Prometheus to scrape WuKongIM metrics; omitted or `0` uses `15s`. |
| `prometheus.scrape_targets` | `WK_PROMETHEUS_SCRAPE_TARGETS` | `string_list` | Metrics targets for managed Prometheus as `host:port` values without a URL scheme and with ports from `1` to `65535`; an empty list may be derived from the API listener. |
## Top [#top]
| TOML | Environment | Type | Description |
| ---------------------- | ------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `top.api_enable` | `WK_TOP_API_ENABLE` | `bool` | Exposes the read-only `/top/v1/snapshot` endpoint on the API listener for `wkcli top`; disabled when omitted and requires non-empty `api.listen_addr`. |
| `top.collect_interval` | `WK_TOP_COLLECT_INTERVAL` | `duration` | Interval at which Top samples local runtime state; omitted or `0` uses `1s`. |
| `top.history_window` | `WK_TOP_HISTORY_WINDOW` | `duration` | In-memory sample window retained for Top queries; omitted or `0` uses `5m` and must be at least twice the collection interval. |
## Diagnostics [#diagnostics]
| TOML | Environment | Type | Description |
| -------------------------------------- | ----------------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `diagnostics.enable` | `WK_DIAGNOSTICS_ENABLE` | `bool` | Captures bounded node-local diagnostic events; enabled when omitted. |
| `diagnostics.buffer_size` | `WK_DIAGNOSTICS_BUFFER_SIZE` | `int` | Maximum diagnostic events retained in memory; `0` uses `50000`. |
| `diagnostics.sample_rate` | `WK_DIAGNOSTICS_SAMPLE_RATE` | `float` | Baseline keep probability for successful diagnostic events, from `0` to `1`; omitted uses `0.01`. |
| `diagnostics.slow_threshold_ms` | `WK_DIAGNOSTICS_SLOW_THRESHOLD_MS` | `int` | Duration threshold for keeping successful slow events, in milliseconds; omitted or `0` uses `500`. |
| `diagnostics.error_sample_rate` | `WK_DIAGNOSTICS_ERROR_SAMPLE_RATE` | `float` | Keep probability for non-success diagnostic events, from `0` to `1`; omitted uses `1`. |
| `diagnostics.deep_sample_rate` | `WK_DIAGNOSTICS_DEEP_SAMPLE_RATE` | `float` | Sampling probability for expensive Reactor or store detail, from `0` to `1`; defaults to `0`. |
| `diagnostics.deep_slow_threshold_ms` | `WK_DIAGNOSTICS_DEEP_SLOW_THRESHOLD_MS` | `int` | Threshold for deep tracing slow Reactor or store stages, in milliseconds; omitted or `0` follows the normal slow threshold. |
| `diagnostics.deep_max_items_per_batch` | `WK_DIAGNOSTICS_DEEP_MAX_ITEMS_PER_BATCH` | `int` | Maximum messages expanded into events by one deep-trace batch; `0` uses `16`. |
| `diagnostics.debug_matches` | `WK_DIAGNOSTICS_DEBUG_MATCHES` | `object_list` | Temporary high-priority sampling rules; an empty list means none. Each item needs at least one of `uid`, `channel_key`, `client_msg_no`, or `trace_id`; positive `ttl_seconds` is required to take effect and zero is silently ignored. `sample_rate` ranges from `0` to `1`, where `0` retains nothing. |
## Gateway [#gateway]
| TOML | Environment | Type | Description |
| ------------------------------------------------------ | --------------------------------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gateway.token_auth_on` | `WK_GATEWAY_TOKEN_AUTH_ON` | `bool` | Requires each CONNECT token to exactly match the stored device token for the same UID and `device_flag`; defaults to `true`. Disable only for a controlled compatibility migration. |
| `gateway.gnet_multicore` | `WK_GATEWAY_GNET_MULTICORE` | `bool` | Enables gnet's CPU-scaled multi-event-loop mode; enabled when omitted. |
| `gateway.gnet_num_event_loop` | `WK_GATEWAY_GNET_NUM_EVENT_LOOP` | `int` | gnet event-loop count; `0` retains the WuKongIM baseline of `4`. |
| `gateway.runtime_async_send_workers` | `WK_GATEWAY_RUNTIME_ASYNC_SEND_WORKERS` | `int` | Maximum workers dispatching SEND frames asynchronously; a non-positive value uses `1000`. |
| `gateway.runtime_async_send_queue_capacity` | `WK_GATEWAY_RUNTIME_ASYNC_SEND_QUEUE_CAPACITY` | `int` | Maximum queued asynchronous SEND frames; a non-positive value uses `131072`, and a full queue rejects new SEND frames. |
| `gateway.runtime_async_auth_workers` | `WK_GATEWAY_RUNTIME_ASYNC_AUTH_WORKERS` | `int` | Maximum workers processing CONNECT authentication asynchronously; a non-positive value uses `16`. |
| `gateway.runtime_async_auth_queue_capacity` | `WK_GATEWAY_RUNTIME_ASYNC_AUTH_QUEUE_CAPACITY` | `int` | Maximum queued CONNECT authentications; a non-positive value uses `8192`, and a full queue rejects new CONNECT authentication requests. |
| `gateway.runtime_async_pool_release_timeout` | `WK_GATEWAY_RUNTIME_ASYNC_POOL_RELEASE_TIMEOUT` | `duration` | Maximum graceful-release wait for asynchronous worker pools during shutdown; a non-positive value uses `100ms`. |
| `gateway.default_session_async_send_batch_max_wait` | `WK_GATEWAY_DEFAULT_SESSION_ASYNC_SEND_BATCH_MAX_WAIT` | `duration` | Maximum wait for a SEND shard to collect adjacent frames; `0` uses `1ms`, while a negative value normalizes to `0` and disables waiting. |
| `gateway.default_session_async_send_batch_max_records` | `WK_GATEWAY_DEFAULT_SESSION_ASYNC_SEND_BATCH_MAX_RECORDS` | `int` | Maximum frames in one Gateway SEND micro-batch; `0` uses `128`. |
| `gateway.default_session_async_send_batch_max_bytes` | `WK_GATEWAY_DEFAULT_SESSION_ASYNC_SEND_BATCH_MAX_BYTES` | `int` | Maximum payload bytes in one Gateway SEND micro-batch; `0` uses `524288`. |
| `gateway.listeners` | `WK_GATEWAY_LISTENERS` | `object_list` | Client listener list: `name`, `network`, `address`, `transport`, and `protocol` are required; `path` and `proxy_protocol_trusted_cidrs` are optional; direct/PROXY v1/v2 detection is enabled by default, with omitted or empty CIDRs accepting unverified address assertions from any peer; names and addresses must each be unique. Omitted opens WKProto TCP on `0.0.0.0:5100` and WSMux on `0.0.0.0:5200`; an explicit empty list (including JSON `null` in the environment) disables Gateway. |
| `gateway.send_timeout` | `WK_GATEWAY_SEND_TIMEOUT` | `duration` | Maximum duration allowed for one message send initiated by Gateway; a non-positive value uses `5s`. |
### Client addresses behind a proxy [#client-addresses-behind-a-proxy]
This capability is currently under **Unreleased**; use a target binary containing
`proxy_protocol_trusted_cidrs`. Older binaries require disabling the PROXY preface
at the upstream proxy.
Every TCP or WebSocket listener automatically accepts plain client traffic and
PROXY protocol v1/v2 on the same port by default, without SDK changes or a proxy
vendor selector. `proxy_protocol_trusted_cidrs` is an optional source filter.
Omitted or empty accepts headers from any peer: connection information and logs
can contain client-declared addresses whose authenticity has not been verified.
Built-in token authentication and message permissions do not rely on those addresses.
To restrict header sources to controlled proxies, configure the filter as follows:
```toml
[[gateway.listeners]]
name = "tcp-wkproto"
network = "tcp"
address = "0.0.0.0:5100"
transport = "gnet"
protocol = "wkproto"
# Replace with the actual controlled proxy egress IP/CIDR.
proxy_protocol_trusted_cidrs = ["192.0.2.10/32"]
```
Use the same field on a `network = "websocket"` listener. At most 128 IPv4/IPv6
CIDRs are accepted; invalid CIDRs fail configuration loading. `WK_GATEWAY_LISTENERS`
accepts this field inside its JSON listener objects and replaces the entire
listener list, including all trust rules.
When setting the filter, match the actual TCP peer as seen by WuKongIM, including
any SNAT. Use controlled proxy egress networks. A trusted proxy must prepend its own header
on forwarded connections; a transparent TCP relay that passes arbitrary client
prefixes through is not a safe trusted source. Listeners with nonempty CIDRs reject
recognized PROXY headers from other peers. Direct traffic remains accepted.
The upstream proxy must support and send the protocol; WuKongIM cannot recover
an address that it did not receive. Parsing occurs before authentication and,
for WebSocket, before HTTP Upgrade. TLS/WSS termination belongs to the upstream
proxy; `X-Forwarded-For` is not used by this feature. It does not change the HTTP
API, Manager, cluster RPC, advertised routes, or token authentication.
Detection has an absolute five-second deadline from TCP accept, including clients
that send no data. Incomplete, malformed, or oversized headers close the connection.
The v1 limit is 107 bytes; v2 permits 4096 bytes including the fixed header and
optional TLVs. TLV values are ignored; supported TCP address blocks validate TLV
framing. `UNKNOWN`, `LOCAL`, and valid unsupported v2 address transports retain
the physical endpoints. Sessions use the asserted client source as `RemoteAddr`,
retain the actual listener as `LocalAddr`, and expose the physical proxy through
`gateway.peer_addr`; connection debug logs include `peerAddr` when it differs.
## Message [#message]
| TOML | Environment | Type | Description |
| ---------------------------------- | ------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `message.person_whitelist_enabled` | `WK_MESSAGE_PERSON_WHITELIST_ENABLED` | `bool` | Enforces receiver-side allowlist checks for person messages; disabled by default for compatibility. |
| `message.cmd_channel_suffix` | `WK_MESSAGE_CMD_CHANNEL_SUFFIX` | `string` | Reserved command-channel suffix; omitted or empty uses `____cmd`. Only ASCII letters, digits, underscores and hyphens are allowed; reserve it from business channel IDs. Must match on every node and requires restart. Choose before first use: changing it does not migrate stored command channels or UID bindings and can make pending commands inaccessible. |
| `message.system_uid` | `WK_MESSAGE_SYSTEM_UID` | `string` | Primary system account UID used when trusted sends omit a sender; omitted or empty uses `____system`. Every cluster node must use the same value. |
| `message.system_device_id` | `WK_MESSAGE_SYSTEM_DEVICE_ID` | `string` | Device ID for trusted system sessions; omitted or empty uses `____device`, so an empty value cannot disable recognition. Such sessions may bypass channel-type send permissions after the send-ban check. |
| `message.permission_cache_ttl` | `WK_MESSAGE_PERMISSION_CACHE_TTL` | `duration` | Cache lifetime for permission, membership, and missing-channel reads; `0` disables caching. |
## Presence [#presence]
| TOML | Environment | Type | Description |
| ------------------------------------- | ---------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `presence.activation_timeout` | `WK_PRESENCE_ACTIVATION_TIMEOUT` | `duration` | Timeout for Gateway session route activation against the UID authority; `0` uses `3s`. |
| `presence.touch_flush_interval` | `WK_PRESENCE_TOUCH_FLUSH_INTERVAL` | `duration` | Interval for flushing local connection activity to UID authorities; `0` uses `1s`. |
| `presence.touch_batch_size` | `WK_PRESENCE_TOUCH_BATCH_SIZE` | `int` | Maximum local touched routes processed per flush chunk; `0` uses `512`. |
| `presence.touch_max_routes_per_flush` | `WK_PRESENCE_TOUCH_MAX_ROUTES_PER_FLUSH` | `int` | Maximum dirty routes processed across all chunks in one flush; omitted uses `65536`, while an explicit value must be positive and at least `presence.touch_batch_size`. |
| `presence.route_ttl` | `WK_PRESENCE_ROUTE_TTL` | `duration` | How long a UID authority keeps a route alive after its latest activity; `0` uses `90s`. |
## Channel Append [#channel-append]
| TOML | Environment | Type | Description |
| --------------------------------------------------------- | ------------------------------------------------------------ | ----- | --------------------------------------------------------------------------------------------------------------- |
| `channel_append.shard_count` | `WK_CHANNEL_APPEND_SHARD_COUNT` | `int` | Shards used to look up Channel append authority state; `0` derives a CPU-aware default. |
| `channel_append.advance_pool_size` | `WK_CHANNEL_APPEND_ADVANCE_POOL_SIZE` | `int` | Worker pool that advances Channel Append Writer state machines; `0` uses the runtime default `500`. |
| `channel_append.effect_pool_size` | `WK_CHANNEL_APPEND_EFFECT_POOL_SIZE` | `int` | Worker pool used by blocking appends and post-append recipient processing; `0` uses the runtime default `2000`. |
| `channel_append.recipient_authority_dispatch_concurrency` | `WK_CHANNEL_APPEND_RECIPIENT_AUTHORITY_DISPATCH_CONCURRENCY` | `int` | Deprecated compatibility input; canonical Online Delivery plans ignore this value. |
## Delivery [#delivery]
| TOML | Environment | Type | Description |
| --------------------------------------- | ------------------------------------------ | ---------- | ------------------------------------------------------------------------------------------------------- |
| `delivery.enable` | `WK_DELIVERY_ENABLE` | `bool` | Connects committed messages to the online-delivery runtime; enabled when omitted. |
| `delivery.fanout_page_size` | `WK_DELIVERY_FANOUT_PAGE_SIZE` | `int` | Maximum subscriber UIDs read in one fanout page; `0` uses `512`. |
| `delivery.push_batch_size` | `WK_DELIVERY_PUSH_BATCH_SIZE` | `int` | Maximum recipients in one exact-target lookup, delivery plan, or owner-node push chunk; `0` uses `512`. |
| `delivery.pending_ack_ttl` | `WK_DELIVERY_PENDING_ACK_TTL` | `duration` | Lifetime used to remove stale pending acknowledgements during delivery activity; `0` uses `30s`. |
| `delivery.pending_ack_max_per_session` | `WK_DELIVERY_PENDING_ACK_MAX_PER_SESSION` | `int` | Maximum owner-local pending acknowledgements retained for one UID session; `0` uses `1024`. |
| `delivery.event_queue_size` | `WK_DELIVERY_EVENT_QUEUE_SIZE` | `int` | Maximum recipient delivery plans waiting for asynchronous processing; `0` uses `1024`. |
| `delivery.recipient_worker_concurrency` | `WK_DELIVERY_RECIPIENT_WORKER_CONCURRENCY` | `int` | Maximum workers processing recipient delivery plans; `0` uses `320`. |
## Webhook [#webhook]
| TOML | Environment | Type | Description |
| --------------------------------------- | ------------------------------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `webhook.http_addr` | `WK_WEBHOOK_HTTP_ADDR` | `string` | Target receiving JSON webhook POST requests; empty disables the webhook runtime. Redacted from diagnostic artifacts. |
| `webhook.focus_events` | `WK_WEBHOOK_FOCUS_EVENTS` | `string_list` | Limits delivery to these names: `msg.notify`, `msg.offline`, or `user.onlinestatus`; an empty list selects all supported events. |
| `webhook.queue_size` | `WK_WEBHOOK_QUEUE_SIZE` | `int` | Maximum webhook events queued in memory before worker execution; `0` uses `1024`. |
| `webhook.workers` | `WK_WEBHOOK_WORKERS` | `int` | Maximum concurrent webhook sender workers; `0` uses `16`. |
| `webhook.msg_notify_batch_max_items` | `WK_WEBHOOK_MSG_NOTIFY_BATCH_MAX_ITEMS` | `int` | Maximum messages in one `msg.notify` webhook request; `0` uses `100`. |
| `webhook.msg_notify_batch_max_wait` | `WK_WEBHOOK_MSG_NOTIFY_BATCH_MAX_WAIT` | `duration` | Maximum wait to collect adjacent `msg.notify` messages; `0` uses `500ms`. |
| `webhook.online_status_batch_max_items` | `WK_WEBHOOK_ONLINE_STATUS_BATCH_MAX_ITEMS` | `int` | Maximum records in one `user.onlinestatus` request; `0` uses `512`. |
| `webhook.online_status_batch_max_wait` | `WK_WEBHOOK_ONLINE_STATUS_BATCH_MAX_WAIT` | `duration` | Maximum wait to collect adjacent `user.onlinestatus` records; `0` uses `2s`. |
| `webhook.offline_uid_batch_size` | `WK_WEBHOOK_OFFLINE_UID_BATCH_SIZE` | `int` | Maximum offline UIDs in one `msg.offline` request; `0` uses `512`. |
| `webhook.request_timeout` | `WK_WEBHOOK_REQUEST_TIMEOUT` | `duration` | Timeout for one outbound webhook request attempt; `0` uses `5s`. |
| `webhook.retry_max_attempts` | `WK_WEBHOOK_RETRY_MAX_ATTEMPTS` | `int` | Maximum attempts for an admitted webhook batch before it is dropped; `0` uses `3`. |
| `webhook.before_send.enabled` | `WK_WEBHOOK_BEFORE_SEND_ENABLED` | `bool` | Independently enable synchronous admission; defaults to false. |
| `webhook.before_send.http_addr` | `WK_WEBHOOK_BEFORE_SEND_HTTP_ADDR` | `string` | HTTP(S) callback endpoint, required when enabled and redacted in diagnostics. |
| `webhook.before_send.timeout` | `WK_WEBHOOK_BEFORE_SEND_TIMEOUT` | `duration` | Callback deadline; zero uses 500ms, capped by the original send deadline. |
| `webhook.before_send.on_timeout` | `WK_WEBHOOK_BEFORE_SEND_ON_TIMEOUT` | `string` | Callback timeout policy: allow or deny; defaults to deny. |
| `webhook.before_send.on_error` | `WK_WEBHOOK_BEFORE_SEND_ON_ERROR` | `string` | Callback error policy: allow or deny; defaults to deny and never overrides explicit rejection. |
| `webhook.before_send.max_in_flight` | `WK_WEBHOOK_BEFORE_SEND_MAX_IN_FLIGHT` | `int` | Per-node concurrency limit; zero uses 256, saturation rejects immediately. |
## Plugin [#plugin]
| TOML | Environment | Type | Description |
| --------------------------------- | ------------------------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `plugin.enable` | `WK_PLUGIN_ENABLE` | `bool` | Enables node-local `.wkp` plugin processes and `PersistAfter` hooks; enabled when omitted. |
| `plugin.dir` | `WK_PLUGIN_DIR` | `string` | Directory containing executable `.wkp` plugin files; when Plugin is enabled, empty derives it from `node.data_dir`. |
| `plugin.socket_path` | `WK_PLUGIN_SOCKET_PATH` | `string` | Unix socket used for plugin host RPC; when Plugin is enabled, empty derives it from `node.data_dir`. |
| `plugin.sandbox_dir` | `WK_PLUGIN_SANDBOX_DIR` | `string` | Root directory for each plugin's writable sandbox data; when Plugin is enabled, empty derives it from `node.data_dir`. |
| `plugin.state_dir` | `WK_PLUGIN_STATE_DIR` | `string` | Directory containing node-local desired plugin state; when Plugin is enabled, empty derives it from `node.data_dir`. |
| `plugin.timeout` | `WK_PLUGIN_TIMEOUT` | `duration` | Timeout for plugin host RPC and graceful process shutdown; `0` uses `5s`. |
| `plugin.hot_reload` | `WK_PLUGIN_HOT_RELOAD` | `bool` | Watches the plugin directory and reloads changed binaries; enabled when omitted. |
| `plugin.fail_open` | `WK_PLUGIN_FAIL_OPEN` | `bool` | Reserved for future Send hooks; omitted is `false`, but the current value has no effect and `PersistAfter` is always fail-open. |
| `plugin.persist_after_queue_size` | `WK_PLUGIN_PERSIST_AFTER_QUEUE_SIZE` | `int` | Maximum `PersistAfter` events queued in memory; `0` uses `1024`. |
| `plugin.persist_after_workers` | `WK_PLUGIN_PERSIST_AFTER_WORKERS` | `int` | Maximum concurrent `PersistAfter` hook workers; `0` uses `16`. |
## Log [#log]
| TOML | Environment | Type | Description |
| ----------------- | -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
| `log.level` | `WK_LOG_LEVEL` | `string` | Minimum recorded level: `debug`, `info`, `warn`, or `error`; omitted uses `info`. |
| `log.dir` | `WK_LOG_DIR` | `string` | Directory for rolling log files; empty uses `./logs`. |
| `log.max_size` | `WK_LOG_MAX_SIZE` | `int` | Maximum log-file size before rotation, in MB; values at or below `0` use `100`. |
| `log.max_age` | `WK_LOG_MAX_AGE` | `int` | Maximum days to retain rotated logs; values at or below `0` use `30`. |
| `log.max_backups` | `WK_LOG_MAX_BACKUPS` | `int` | Maximum rotated files retained per log; values at or below `0` use `10`. |
| `log.compress` | `WK_LOG_COMPRESS` | `bool` | Compresses rotated log files with gzip; enabled when omitted. |
| `log.console` | `WK_LOG_CONSOLE` | `bool` | Adds a standard-output log sink; enabled when omitted. |
| `log.format` | `WK_LOG_FORMAT` | `string` | File encoder format; `json` emits structured JSON, while other values use console encoding. Omitted uses `console`. |
## Migrating removed environment variables [#migrating-removed-environment-variables]
These legacy variables are no longer accepted. Replace them when upgrading configuration:
| Removed | Replacement |
| ---------------------------------------- | ---------------------------------------------------- |
| `WK_CLUSTER_GROUP_COUNT` | `WK_CLUSTER_INITIAL_SLOT_COUNT` |
| `WK_CLUSTER_GROUP_REPLICA_N` | `WK_CLUSTER_SLOT_REPLICA_N` |
| `WK_CLUSTER_HASH_SLOT_MIGRATION_ENABLED` | Use the applicable `WK_CHANNEL_MIGRATION_*` settings |
## MQTT (development branch) [#mqtt-development-branch]
Disabled by default. Full MQTT acceptance is in progress; these settings support development validation.
See the [MQTT topic](/en/api/client-protocols/mqtt) and [quickstart](/en/guide/tutorials/mqtt); cluster consistency and quota interpretation are covered in [operations and troubleshooting](/en/api/client-protocols/mqtt/operations-and-troubleshooting).
| Key | Environment variable | Type | Description |
| ------------------------------- | ---------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `mqtt.enable` | `WK_MQTT_ENABLE` | `bool` | Enable MQTT business handling and the default TCP listener; additional WebSocket listeners use `gateway.listeners` with `protocol="mqtt"`. Default false; MQTT product acceptance remains in progress. |
| `mqtt.listen_addr` | `WK_MQTT_LISTEN_ADDR` | `string` | TCP bind address with a nonzero port; TLS terminates upstream. |
| `mqtt.namespace` | `WK_MQTT_NAMESPACE` | `string` | Stable broker ClientID namespace; use the same value on every cluster node. |
| `mqtt.max_connections` | `WK_MQTT_MAX_CONNECTIONS` | `int` | Maximum retained owners including pending and closing connections; 1 to 1000000. |
| `mqtt.max_subscriptions` | `WK_MQTT_MAX_SUBSCRIPTIONS` | `int` | Maximum pending, active and removing subscriptions per Session; 1 to 1024. |
| `mqtt.workers` | `WK_MQTT_WORKERS` | `int` | Workers in each bounded connection/delivery cohort; 1 to 128. |
| `mqtt.max_packet_bytes` | `WK_MQTT_MAX_PACKET_BYTES` | `uint32` | Maximum inbound packet bytes; default and ceiling 1 MiB. |
| `mqtt.session_expiry_limit_sec` | `WK_MQTT_SESSION_EXPIRY_LIMIT_SEC` | `uint32` | Maximum requested offline Session lifetime in seconds; default 24 hours. |
| `mqtt.quota_messages` | `WK_MQTT_QUOTA_MESSAGES` | `uint64` | Logical per-Session backlog count limit; candidate default 10000, not a scale guarantee. |
| `mqtt.quota_bytes` | `WK_MQTT_QUOTA_BYTES` | `uint64` | Logical per-Session backlog byte limit; candidate default 64 MiB. |
| `mqtt.storage_node_bytes` | `WK_MQTT_STORAGE_NODE_BYTES` | `uint64` | Per-node reservations for shared originals and future replay copies; default 8 GiB, independent of Session quotas. |
| `mqtt.storage_cluster_bytes` | `WK_MQTT_STORAGE_CLUSTER_BYTES` | `uint64` | Sum of replica reservations across nodes; default 64 GiB, identical on every storage node, including nodes with MQTT disabled. Excludes ordinary history, WAL and physical amplification. |
| `mqtt.window_limit` | `WK_MQTT_WINDOW_LIMIT` | `uint16` | Maximum durable QoS 1 exchanges; 1 to 1024, further limited by peer Receive Maximum. |
# Security & Access (/en/server/configuration/security)
Separate three entries: CONNECT uses device tokens, Manager uses its own JWT, and WuKongIM HTTP API relies on trusted networking or external business auth.
## Values to protect [#values-to-protect]
Replace example users, JWT secrets, and join tokens. Inject from a secret system and restrict reads; redaction does not replace log review or least privilege.
Complete credentials, risks, and handling
Configuration security starts with who can reach each traffic plane, not merely with moving passwords into environment variables. Give clients, WuKongIM HTTP API, node Transport, Manager, and observability separate trust boundaries.
| Configuration | Risk | Production handling |
| ------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `gateway.token_auth_on` | Disabling it bypasses CONNECT device-token validation | Keep the default `true`; disable only for a time-bounded compatibility migration with rollback |
| `cluster.join_token` | Grants seed-join capability | Inject from a secret system, restrict reads, and support rotation |
| `manager.jwt_secret` | Signs Manager sessions | Use an independent high-entropy secret and audit rotation |
| `manager.users` | Administrative accounts, passwords, and permissions | Remove example users and grant least privilege |
| `bench.api_token` | Authorizes a high-load benchmark API | Disable by default; require a capability token whenever remotely reachable |
| External URLs and match configuration | May reveal internal topology or diagnostic targets | Treat as sensitive in diagnostic artifacts and support bundles |
The startup snapshot redacts fields declared sensitive, and diagnostic output also redacts diagnostic-sensitive fields. This does not replace log review, a secret store, or least-read permissions.
## Client CONNECT token authentication [#client-connect-token-authentication]
`gateway.token_auth_on` defaults to `true`. Gateway reads the device record persisted by `/user/token` for the same `UID + device_flag` and compares the token exactly. An empty token, missing or cleared record, or mismatch returns `ReasonAuthFail`; after a match, the Session adopts the stored Device Level.
**Verify:** the startup snapshot shows `gateway.token_auth_on=true`; `WK_GATEWAY_TOKEN_AUTH_ON` overrides TOML. Valid device tokens pass this check; wrong, missing, or revoked tokens return `ReasonAuthFail`. This switch does not protect WuKongIM HTTP API.
Explicit TOML, environment override, and migration constraints
Keep the secure default explicit in deployment configuration so upgrades and environment differences are unambiguous:
```toml
[gateway]
token_auth_on = true
```
The equivalent environment variable is `WK_GATEWAY_TOKEN_AUTH_ON=true`, and it overrides TOML. Audit the effective value in the startup configuration snapshot. Setting it to `false` lets new CONNECT requests bypass this device-token check and is only for a time-bounded compatibility migration with a rollback plan; it does not add authentication to WuKongIM HTTP API.
## Manager and WuKongIM HTTP API [#manager-and-wukongim-http-api]
### Manager [#manager]
Enable `manager.auth_on`, replace JWT secrets and users, and grant only needed resources/actions. Use loopback SSH or private/VPN HTTPS. Restrict node `5301` and proxy upstream sources; never public access or production wildcard grants.
### WuKongIM HTTP API [#wukongim-http-api]
**No general business token validation.** Manager login does not protect WuKongIM HTTP API; use trusted networking or an external proxy with business identity checks.
Full Manager permissions and WuKongIM HTTP API boundaries
Enable `manager.auth_on` in production and replace example JWT secrets and users. Access Manager through an SSH tunnel to loopback, or an HTTPS proxy on a management network/VPN. Do not expose node port `5301` publicly; restrict sources on the proxy-to-node path too. Grant only required resources and actions; the example global wildcard is not a production baseline. See [Manager](/en/server/operations/manager) for access steps.
WuKongIM HTTP API routes do not provide general business token validation. Manager authentication does not protect the WuKongIM HTTP API. Put business routes on a trusted network or behind an API gateway, reverse proxy, and business identity check.
## TLS and endpoint exposure [#tls-and-endpoint-exposure]
Match TCP/TLS and WS/WSS addresses to certificates; protect proxy-to-node links. Disable unused Benchmark and Debug. Restrict metrics, Top, diagnostics, pprof, and Transport separately, with authorization and access expiry.
Complete TLS and endpoint-exposure notes
Production TLS is commonly terminated by a load balancer, reverse proxy, or service mesh. Verify that client TCP/TLS and WS/WSS addresses match certificates, and protect the upstream link from the proxy to each node.
Disable unused Benchmark and Debug capabilities. `/metrics`, Top, diagnostics, pprof, Manager, and node Transport need their own network policy, authorization, audit, and temporary-access expiry even when they share a host.
## TOML, environment, and validation [#toml-environment-and-validation]
Unknown TOML paths or `WK_*` fail startup. Environment values override TOML; JSON replaces whole lists. Audit both sources. Never commit real secrets; environment variables are only an injection channel.
Full secret-storage and configuration-validation boundaries
* TOML files and deployment templates may enter artifacts, backups, or code review; never commit real secrets.
* Environment variables may appear in process inspection, crash reports, or a platform control plane. Treat them as an injection channel, not a complete secret-management system.
* Unknown TOML paths and unknown `WK_*` environment variables fail startup, preventing misspellings from silently doing nothing.
* Environment values override TOML. List values are JSON and replace the complete list, so deployment audits must inspect both sources.
## Rotation [#rotation]
Check what the field affects and whether it explicitly supports old/new overlap. Otherwise, plan per-node restarts with interruption and rollback.
**Verify:** each node returns `200` with `ready: true` from `/readyz`; peer connections, Manager login, messaging, and collection pass before restoring traffic.
Full rotation process and implementation ownership
Before rotation, determine whether a field affects peer trust, sessions, or external clients. Prepare an overlap window that accepts old and new values, or schedule a controlled rolling restart with an explicit interruption and rollback. Wait for `/readyz` before returning each node to traffic, then verify peer connections, Manager login, product messaging, and observability collection.
See [Configuration Reference](/en/server/configuration/reference) for redaction flags. Secret distribution, certificates, and firewall implementation remain responsibilities of the deployment platform.
# Messages & Storage (/en/server/configuration/storage)
Give each node exclusive durable storage, then compare retention, queues, batches, and concurrency under the same workload.
## Data directories [#data-directories]
### Node A [#node-a]
Exclusive directory retaining node state.
### Node B [#node-b]
Exclusive directory preserving failure isolation.
`node.data_dir` is required at startup and cannot be shared across nodes. The diagram shows ownership; additional log, Prometheus, and plugin paths are configured separately. After replacement or rollout, required directories must survive with correct UID/GID, permissions, capacity, and mounts.
Complete directories, additional paths, and persistence rules
Storage settings affect data safety, throughput, and tail latency together. Give every node independent durable storage first, then tune retention, queues, batches, and concurrency from observed evidence.
`node.data_dir` is required at startup and owns node state. Every node needs its own directory or persistent volume; sharing one directory across nodes breaks ownership and failure isolation.
Related paths include:
* `log.dir` for rolling logs;
* `prometheus.data_dir` for generated configuration and TSDB data only when app-managed Prometheus is used;
* `plugin.dir`, `plugin.state_dir`, `plugin.sandbox_dir`, and `plugin.socket_path` for plugin artifacts, state, sandboxing, and the local socket.
Directories that must survive need to remain available after container replacement, process restart, or artifact rollout, with the correct UID/GID, permissions, capacity, and mount options.
## Message retention [#message-retention]
Before enabling physical GC through `channel.message_retention_*`:
1. Define retention, recovery objectives, and backup boundaries.
2. Test scan interval, channels per batch, and message/byte limits at representative data scale.
3. Confirm required history remains readable.
Larger batches also raise IO bursts, lock duration, memory, and foreground tail latency.
Complete retention and physical trimming boundaries
`channel.message_retention_*` controls scanning and physical trimming. Before enabling physical GC, define the business retention period, recovery objectives, and backup boundary, then verify trimming preserves required historical reads.
Scan interval, channels per batch, and per-run message and byte limits bound background work together. Larger batches may improve throughput while increasing IO bursts, lock duration, memory, and foreground tail latency. Validate production values at representative data scale.
## Queues, batches, and concurrency [#queues-batches-and-concurrency]
Observe queues, workers, and batches together. Larger queues increase memory and worst-case wait, and may hide sustained overload. Some `0` values are derived at runtime; they do not mean unlimited or disabled.
The Manager startup snapshot covers only a bounded set of critical fields: inspect source and effective value where available; use runtime state, metrics, and pre-production tests elsewhere. Explicit examples are not defaults for omitted fields.
Complete tuning domains, fields, and costs
| Domain | Representative fields | Main cost |
| ----------------------------- | ----------------------------------------------------- | ---------------------------------------------------------- |
| Gateway | async workers, queue capacity, session batches | Connection memory, queue delay, send throughput |
| Channel append | shards, pools, batches, dispatch concurrency | CPU contention, commit latency, allocations, disk batches |
| Delivery | page and push batches, event queue, recipient workers | Large-group fanout, pending acknowledgements, backpressure |
| Membership directory/Presence | paged sync and hydration batches, touch batches | Candidate bounds, DB reads, remote batch calls |
| Webhook | queue, workers, batches, timeout, retry | Downstream failure amplification, memory, drop boundaries |
Queue capacity is not capacity planning. A larger queue absorbs a larger transient burst, but also consumes more memory, increases worst-case wait, and may hide sustained overload. Worker and batch limits must be observed with it.
Some `0` values mean derive from CPU, topology, or an internal default; they do not mean unlimited or disabled. The Manager startup snapshot resolves a bounded set of critical fields: use its source and effective value where available, and confirm other fields through subsystem runtime state, metrics, and pre-production load tests. Do not treat explicit values in `wukongim.toml.example` as defaults for every omitted field.
## Capacity and alerts [#capacity-and-alerts]
Record disk space/inodes, IO and sync latency, queues/rejections, P95/P99, CPU/RSS/FD/goroutines, network, and large-group fanout per node. Retain artifact, configuration, hardware, users, message rate, channels, and group size with each test.
**Acceptance:** roll one node at a time; `/readyz` must return `200` with `ready: true` before restoring traffic. Compare latency, queues, resources, and errors. Keep rollback configuration; a binary downgrade does not guarantee data-format rollback.
Complete capacity, changes, and production validation
At minimum, monitor disk space and inodes, IOPS and latency, write and sync latency, queue depth and rejection, P95/P99, CPU, RSS, file descriptors, goroutines, network, and large-group fanout per node. Capacity tests must record artifact, configuration, hardware, online users, message rate, channel count, and group size.
Roll every setting change one node at a time, wait for `/readyz` before restoring traffic, and compare latency, queues, resources, and errors before and after. Retain rollback configuration, but do not assume a binary downgrade proves data-format rollback is safe.
Before production, validate an archive with [Backup & Restore](/en/server/operations/backup-and-restore) and use [`wkcli bench`](/en/server/tools/wkbench) to test capacity with a representative workload.
# Docker (/en/server/deployment/docker)
Start a single-node cluster on your computer, then open the Chat Demo. You need Docker and a browser.
The example pins image `3.0.0-beta.24`. To use another release, replace the image tag directly and read that release's upgrade notes first. Before serving production traffic, replace the example credentials and complete [Security & Access](/en/server/configuration/security) and [Health & Monitoring](/en/server/operations/health-and-monitoring).
## 1. Create the configuration file [#1-create-the-configuration-file]
```bash
mkdir -p wukongim-docker
cd wukongim-docker
```
Create `wukongim.toml`:
```toml
[node]
id = 1
data_dir = "/var/lib/wukongim"
[cluster]
listen_addr = "127.0.0.1:7001"
[api]
listen_addr = "0.0.0.0:5001"
[gateway]
token_auth_on = true
[manager]
listen_addr = "0.0.0.0:5301"
auth_on = true
jwt_secret = "replace-with-a-random-64-character-secret"
users = [{ username = "admin", password = "replace-with-a-strong-password", permissions = [{ resource = "*", actions = ["*"] }] }]
[log]
dir = "/var/lib/wukongim/logs"
```
Replace `jwt_secret` and the administrator password. Defaults use 256 hash slots; keep `gateway.token_auth_on=true`.
Configuration fields and token checks
This file explicitly keeps the important `gateway.token_auth_on=true` setting: a client CONNECT token must exactly match the token stored through `/user/token` for the same UID and device category, or Gateway returns `ReasonAuthFail`. Do not disable it in production merely to simplify integration; see [Security & Access](/en/server/configuration/security) for the full boundary. The remaining fields cover only container startup, the API, Manager authentication, and a writable log directory. Single-node topology, 256 hash slots, the `5100/5200` Gateway listeners, and other settings use runtime defaults. Replace at least `jwt_secret` and the administrator password. When accessing the Demo through matching host ports, `api.external_tcp_addr` and `api.external_ws_addr` can be omitted; see the address rules below. See the [Configuration Reference](/en/server/configuration/reference) for every field and its default.
Linux hosts: set file permissions before starting
The official image runs as the non-root user `10001:10001`. With a bind mount, that user must be able to read the host's `wukongim.toml`. Keep write access for the user who created the file and grant read-only access to the container's group:
```bash
sudo chown "$(id -u):10001" wukongim.toml
chmod 0640 wukongim.toml
```
Do not leave the file as root-only `0600`; the container will repeatedly restart after failing to read its configuration with `permission denied`. The file contains the Manager password and JWT secret, so making it world-readable with `0644` is also discouraged. If you change the image user or use rootless Docker or user-namespace UID/GID mappings, grant read access according to the effective mapping instead.
## 2. Start and verify [#2-start-and-verify]
Official images are published to both GHCR and Alibaba Cloud Container Registry with identical image contents for the same version. Users in mainland China should use the Alibaba Cloud image:
| Registry | Image address |
| ---------------------------------------------- | ------------------------------------------------------------------- |
| GHCR | `ghcr.io/wukongim/wukongim:3.0.0-beta.24` |
| Alibaba Cloud (recommended for mainland China) | `registry.cn-shanghai.aliyuncs.com/wukongim/wukongim:3.0.0-beta.24` |
The examples below use GHCR. If the pull fails, replace the image address on the last line of `docker run` or the `image` value in `compose.yaml` with the Alibaba Cloud address above, keeping the same version tag.
Start with `docker run` below; Docker Compose is an alternative.
### Use `docker run` [#use-docker-run]
```bash
docker run -d --name wukongim --restart unless-stopped \
-p 127.0.0.1:5001:5001 -p 5100:5100 -p 5200:5200 -p 127.0.0.1:5301:5301 \
-v "$PWD/wukongim.toml:/etc/wukongim/wukongim.toml:ro" \
-v wukongim-data:/var/lib/wukongim \
ghcr.io/wukongim/wukongim:3.0.0-beta.24
```
Use Docker Compose instead
Create `compose.yaml`:
```yaml
services:
wukongim:
image: ghcr.io/wukongim/wukongim:3.0.0-beta.24
container_name: wukongim
restart: unless-stopped
ports: ["127.0.0.1:5001:5001", "5100:5100", "5200:5200", "127.0.0.1:5301:5301"]
volumes: ["./wukongim.toml:/etc/wukongim/wukongim.toml:ro", "wukongim-data:/var/lib/wukongim"]
volumes:
wukongim-data:
name: wukongim-data
```
```bash
docker compose up -d
```
Both methods create `wukongim-data` automatically on the first run, so no separate volume command is needed.
Wait for the container to become healthy, then verify the node:
```bash
docker ps --filter name=wukongim
docker exec wukongim wget -q --spider -T 5 http://127.0.0.1:5001/readyz
```
After readiness succeeds, open [Chat Demo](http://127.0.0.1:5001/demo/) and [send the first message](/en/guide/quick-start/first-message).
Manager is at [http://127.0.0.1:5301](http://127.0.0.1:5301); sign in as `admin` with the configured password. See [Quick Start](/en/guide/quick-start) for the full learning path.
How do I open Manager and the Demo from my computer after deploying on a server?
Manager and the WuKongIM HTTP API are mapped only to the server loopback address. Create an SSH tunnel including the Demo WebSocket port, then open the same Manager URL or the [Chat Demo](http://127.0.0.1:5001/demo/) on your computer:
```bash
ssh -N \
-L 5001:127.0.0.1:5001 \
-L 5200:127.0.0.1:5200 \
-L 5301:127.0.0.1:5301 \
user@server
```
Connection addresses, port mappings, and remote access
When `api.external_*` is omitted, default external `/route` and `/route/batch` replace a listener host of `0.0.0.0`, `[::]`, or an empty host with the hostname from the current request Host, preserving the Gateway port, scheme, and path. Opening `http://127.0.0.1:5001/demo/` therefore yields `ws://127.0.0.1:5200`; accessing the API directly through a server IP or domain yields that host with port `5200`.
This applies only to the default external route. Explicit published addresses, concrete listener hosts (including Linux initialization's `127.0.0.1`), intranet requests, and explicit node selectors stay unchanged. Proxies must preserve the original Host; the server does not use `X-Forwarded-Host` or infer WSS from the HTTP scheme.
For a mapping such as `15200:5200`, a separate WebSocket domain or TLS proxy, or a backend calling `/route` through an internal host unreachable by clients, set a browser-reachable `api.external_ws_addr` or `api.external_wss_addr`. Use `api.external_tcp_addr` for a non-default TCP ingress. Completion does not open ports or create TLS listeners.
Enable bundled Prometheus when needed
Use an image whose release notes include the Docker bundled Prometheus fix. Add the following to `wukongim.toml`, then restart the container:
```toml
[prometheus]
enable = true
[observability]
metrics_enable = true
```
`metrics_enable` exposes WuKongIM's `/metrics`; `prometheus.enable` starts a Prometheus child process managed by WuKongIM. Images carry the matching binary for `linux/amd64` and `linux/arm64`, so `binary_path` can stay empty and startup needs no download.
By default, Prometheus scrapes `127.0.0.1:5001/metrics` inside the container and listens on `127.0.0.1:9099`. Manager automatically queries that address. The bundled process provides collection and query APIs; view metrics through Manager. The image does not include Prometheus's own web UI. Data lives in `/var/lib/wukongim/prometheus` on the existing `wukongim-data` Volume. Retention defaults to 15 days; adjust `prometheus.retention_time` and `prometheus.retention_size` as needed.
```bash
docker restart wukongim
docker exec wukongim wget -qO- http://127.0.0.1:9099/-/ready
docker exec wukongim wget -qO- 'http://127.0.0.1:9099/api/v1/query?query=up%7Bjob%3D%22wukongim%22%7D'
```
After readiness, allow one default 15-second scrape interval. The query result's `value` should contain `"1"`. These checks run inside the container and need no additional host port mapping. WuKongIM stops the child process when the container stops. Reuse the same data volume when recreating the container to retain historical metrics.
If an older image reports `embedded prometheus binary missing`, upgrade to an image containing this fix and recreate the container; restarting the old image cannot supply the missing binary. With an external Prometheus service, keep `prometheus.enable=false` and set `prometheus.query_base_url` as needed.
Common operations
```bash
docker logs --follow --tail 100 wukongim
docker restart wukongim
docker stop wukongim
docker start wukongim
```
Removing the container does not remove `wukongim-data`. Before upgrading, read [Upgrades and Migration](/en/server/operations/upgrade-and-migration), confirm compatibility, and then recreate the container with the new image.
Remove the evaluation deployment completely
For a Docker Compose deployment, after confirming that the data is no longer needed, run:
```bash
docker compose down --volumes
rm -f wukongim.toml compose.yaml
```
For a `docker run` deployment, run:
```bash
docker rm --force wukongim
docker volume rm wukongim-data
rm wukongim.toml
```
These commands permanently remove message data and deployment credentials.
A production deployment also needs TCP/TLS and WSS endpoints, restricted access to ports `5001` and `5301`, plus backup and rollback procedures. Continue to [Multi-node Cluster](/en/server/deployment/multi-node) when you need replication and failure recovery.
# Deployment (/en/server/deployment)
Deployment starts with one decision: **can you accept a single point of failure?** If you are unsure which runtime to choose, start with Docker.
A one-node deployment is a single-node cluster. It still uses the Controller, Slot, Channel, routing, and storage paths; there is no standalone mode that bypasses cluster semantics.
## Choose a path [#choose-a-path]
| Your goal | Start here |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------- |
| Run locally and verify the message path | [Quick Start](/en/guide/quick-start) |
| Use the shortest repeatable deployment path | [Docker](/en/server/deployment/docker) (recommended) |
| Manage hosts, files, and systemd directly | [Linux](/en/server/deployment/linux) |
| Replicate across nodes and plan failure recovery | Read [Multi-node Cluster](/en/server/deployment/multi-node), then choose Docker or Linux |
A single-node cluster fits development, validation, and small production workloads that explicitly accept interruption after a node or disk failure. Use a multi-node cluster across independent failure domains when service must continue after a node failure.
## Every deployment must satisfy [#every-deployment-must-satisfy]
* Pin an artifact version or digest; never use a floating tag.
* Give every node a unique ID, independent persistent storage, and a reachable transport address.
* Replace example credentials and restrict Manager, metrics, debug, benchmark, diagnostics, and transport access.
* Advertise TCP, WS, or WSS addresses that clients can actually reach; use `0.0.0.0` only for listeners.
* Verify persistent mounts, an end-to-end message, and `/readyz` before admitting traffic.
```bash
curl --fail http://127.0.0.1:5001/readyz
```
`/healthz` only proves that the process responds. Load balancers, release gates, and traffic cutover must use `/readyz`.
# Linux (/en/server/deployment/linux)
Install a single-node cluster with APT or DNF, then start it with systemd. Current preview: `v3.0.0-beta.24` · amd64/x86\_64.
Validate it outside production before serving production traffic.
Supported distributions and architecture
The preview repository supports amd64/x86\_64. It is verified on Ubuntu 24.04, Debian 13, Rocky Linux 9, and AlmaLinux 9; RHEL 9 can use the same EL9 repository.
## 1. Install [#1-install]
Choose your distribution and expand its commands.
### Debian / Ubuntu [#debian--ubuntu]
APT installation commands
```bash
curl -fsSL https://packages.githubim.com/repo | sudo sh
sudo apt update
sudo apt install -y wukongim
```
### RHEL / Rocky / AlmaLinux 9 [#rhel--rocky--almalinux-9]
DNF installation commands
```bash
curl -fsSL https://packages.githubim.com/repo | sudo sh
sudo dnf -y --disablerepo='*' --enablerepo=wukongim-preview makecache --refresh
sudo dnf install -y wukongim
```
**Expected:** `wukongim` and `wkcli` have matching `version`, `commit`, and `build_source`; the service has not started yet.
Verify binary identities and installation paths
The same package installs the server and the unified operator CLI. Their `version`, `commit`, and `build_source` fields must match. See [wkcli](/en/server/tools/wkcli) for command usage. If `wkcli` is missing or its identity differs, check `command -v wkcli` for an older binary on `PATH` and update to the matching package.
```bash
wukongim version --output json
wkcli version --output json
```
The package creates the service account, systemd service, `/etc/wukongim`, `/var/lib/wukongim`, and `/var/log/wukongim`. It does not create a configuration or start the service.
## 2. Initialize the configuration [#2-initialize-the-configuration]
Run `sudo wukongim init`, **save the password shown only once**, then validate the config. Listeners use loopback by default; configure the firewall, TLS, and ingress before exposing them.
**Expected:** `/etc/wukongim/wukongim.toml` exists and configuration validation succeeds.
Initialize, validate, automate, and forward Demo ports
```bash
sudo wukongim init
# For automation:
sudo wukongim init \
--admin-password-stdin < /secure/path/manager-password
```
Save the administrator password, which is shown only once. Existing automation may keep using `wukongim config init --config /etc/wukongim/wukongim.toml`. Then validate the configuration:
```bash
sudo wukongim config validate --config /etc/wukongim/wukongim.toml
```
Services bind to loopback by default. Configure the firewall, TLS, and ingress policy before exposing them.
Initialization binds WebSocket to `127.0.0.1:5200`, which `/route` preserves as a concrete listener address. Forward `5001`, `5200`, and `5301` as shown in [Quick Start](/en/guide/quick-start/single-node-cluster) to use the Demo without `api.external_ws_addr`. If the listener is changed to a wildcard, default external routing can complete its host from the request. Remapped external ports, separate domains, and TLS proxies still require explicit published addresses; see the [Docker address rules](/en/server/deployment/docker).
## 3. Start and verify [#3-start-and-verify]
Start the systemd service and check `/readyz`. **Admit traffic only after `200` with `ready: true`**; inspect service logs if the check fails.
Start, check readiness, and inspect logs
```bash
sudo systemctl enable --now wukongim
curl --fail http://127.0.0.1:5001/readyz
```
Add the node to traffic only after `/readyz` succeeds. If startup fails, inspect the logs:
```bash
sudo journalctl -u wukongim -n 100 --no-pager
```
**Open Manager:** create an SSH tunnel from your computer, then visit [127.0.0.1:5301](http://127.0.0.1:5301). Sign in as `admin` with the saved password. Keep `auth_on = true`; port `5301` needs no public access.
SSH tunnel and shared administration
The default Manager address is [http://127.0.0.1:5301](http://127.0.0.1:5301). Sign in as `admin` with the password saved during initialization and keep `auth_on = true`. For remote access, run the following on your computer, keep the terminal open, and open the same browser address:
```bash
ssh -N -L 127.0.0.1:5301:127.0.0.1:5301 @
```
Port `5301` does not need a public firewall rule. For shared administration, use a management network or VPN with an HTTPS proxy; see [Manager](/en/server/operations/manager).
For multiple nodes, continue to [Multi-node Cluster](/en/server/deployment/multi-node). Before upgrading, read [Upgrade & Migration](/en/server/operations/upgrade-and-migration).
# Multi-node Cluster (/en/server/deployment/multi-node)
A multi-node cluster simply means **running the same WuKongIM service on several servers and keeping copies of data between them.** This example uses three servers.
## 1. Prepare three servers [#1-prepare-three-servers]
| Server | Node ID | Address between nodes |
| -------- | ------- | --------------------- |
| Server 1 | `1` | `10.0.0.11:7000` |
| Server 2 | `2` | `10.0.0.12:7000` |
| Server 3 | `3` | `10.0.0.13:7000` |
Before you start, check three things:
* all three servers run the same WuKongIM version;
* each server has its own persistent disk and does not share its data directory;
* the servers can reach each other on port `7000` over the private network.
## 2. Copy one cluster configuration [#2-copy-one-cluster-configuration]
| Setting | Value across the three servers |
| -------------- | ---------------------------------------------------------------------- |
| `node.id` | `1`, `2`, and `3` respectively |
| `[cluster]` | Identical; use one random `join_token`, 256 hash slots, three replicas |
| Local listener | `0.0.0.0:7000` is valid |
| Peer addresses | Real private addresses; never `0.0.0.0` or `127.0.0.1` |
Complete cluster TOML and copy steps
Put these sections in `wukongim.toml` on every server. This is the configuration for server 1:
```toml
[node]
id = 1
data_dir = "/var/lib/wukongim"
[cluster]
id = "prod-im-a"
listen_addr = "0.0.0.0:7000"
join_token = "replace-with-one-random-secret"
hash_slot_count = 256
slot_replica_n = 3
channel_replica_n = 3
nodes = [
{ id = 1, addr = "10.0.0.11:7000" },
{ id = 2, addr = "10.0.0.12:7000" },
{ id = 3, addr = "10.0.0.13:7000" }
]
```
Copy the file to servers 2 and 3, changing only `node.id` to `2` and `3`. The `[cluster]` section must be identical on every server. Replace `join_token` with the same random secret on all three.
`listen_addr` tells the current server where to listen, so `0.0.0.0` is valid there. The addresses in `nodes` tell the other servers how to connect, so they must be real private IP addresses—not `0.0.0.0` or `127.0.0.1`. See [Nodes & Cluster](/en/server/configuration/cluster) for other settings.
With one node offline, existing channels can commit only when quorum and runtime conditions hold. New placement may fail and remaining nodes may return `503` from `/readyz`. Test existing traffic and new placement separately. Use **at least four servers** to retain new three-replica placement and full readiness with one offline.
Full three-replica failure-test constraints
A three-replica channel has three nodes in its target replica set. With one node offline, existing channels can still commit when quorum and runtime requirements are met. New channel placement still requires enough eligible nodes, and `/readyz` checks that condition, so the remaining nodes may return `503`. Test existing-channel traffic and new-channel placement separately; neither proves the other. Use at least four servers if new three-replica channels and full readiness must remain available with one server offline.
## 3. Start and check the servers [#3-start-and-check-the-servers]
Start all three using [Docker](/en/server/deployment/docker) or [Linux](/en/server/deployment/linux), then verify:
| Check | Required result |
| ----------------------- | --------------------------------------------------------- |
| `/readyz` on every node | All return `200` with `{"ready":true}` |
| Two-client test | One sends, the other receives; sync works after reconnect |
| Load-balancer pool | Only nodes passing `/readyz` receive traffic |
Per-node commands and message validation
Start WuKongIM on all three servers using the [Docker](/en/server/deployment/docker) or [Linux](/en/server/deployment/linux) deployment, then check each one:
```bash
curl --fail http://10.0.0.11:5001/readyz
curl --fail http://10.0.0.12:5001/readyz
curl --fail http://10.0.0.13:5001/readyz
```
After all three return `200` with `{"ready":true}`, send one test message. Confirm that another client receives it and can sync it after reconnecting. Finally, configure the load balancer to send traffic only to nodes whose `/readyz` check succeeds.
### Open Manager [#open-manager]
Keep the JWT secret, admin password, and auth config identical on all nodes, with `auth_on = true`. Access Manager through a private network or VPN. Node port `5301` allows only the proxy and authorized admins; never public access.
Complete Manager config and sign-in steps
All three nodes must use the same Manager authentication configuration. Otherwise, login fails when Nginx sends a request to another node:
```toml
[manager]
listen_addr = "0.0.0.0:5301"
auth_on = true
jwt_secret = "replace-with-the-same-random-64-character-secret"
users = [{ username = "admin", password = "replace-with-the-same-strong-password", permissions = [{ resource = "*", actions = ["*"] }] }]
```
Replace `jwt_secret` and the password with strong random values, then keep them identical on all three nodes. After the Nginx configuration below is active, open `https://manager.internal.example.com:5301` and sign in as `admin` with the configured password. See [Manager](/en/server/operations/manager) for details.
### Add Nginx [#add-nginx]
Configure TCP / WSS and private Manager ingress, publish the same edge address on every node, then validate and reload Nginx. Stream / Stream SSL modules are required; `stream` belongs at the top level.
**Admission:** open-source Nginx does not actively check `/readyz`; remove unready nodes from `upstream`. One proxy is a single point of failure; use two behind one entry or a managed load balancer in production.
Complete Nginx, published addresses, and reload steps
This Nginx configuration distributes client TCP, WSS, and Manager connections. It requires the Nginx Stream and Stream SSL modules. Put `stream` at the top level of `nginx.conf`, not inside `http`:
```nginx
stream {
upstream wukongim_tcp {
least_conn;
server 10.0.0.11:5100 max_fails=2 fail_timeout=10s;
server 10.0.0.12:5100 max_fails=2 fail_timeout=10s;
server 10.0.0.13:5100 max_fails=2 fail_timeout=10s;
}
upstream wukongim_ws {
least_conn;
server 10.0.0.11:5200 max_fails=2 fail_timeout=10s;
server 10.0.0.12:5200 max_fails=2 fail_timeout=10s;
server 10.0.0.13:5200 max_fails=2 fail_timeout=10s;
}
upstream wukongim_manager {
least_conn;
server 10.0.0.11:5301 max_fails=2 fail_timeout=10s;
server 10.0.0.12:5301 max_fails=2 fail_timeout=10s;
server 10.0.0.13:5301 max_fails=2 fail_timeout=10s;
}
server {
listen 5100;
proxy_pass wukongim_tcp;
proxy_connect_timeout 5s;
proxy_timeout 1h;
}
server {
listen 443 ssl;
ssl_certificate /etc/nginx/certs/im.example.com.crt;
ssl_certificate_key /etc/nginx/certs/im.example.com.key;
proxy_pass wukongim_ws;
proxy_connect_timeout 5s;
proxy_timeout 1h;
}
server {
listen 10.0.0.10:5301 ssl;
ssl_certificate /etc/nginx/certs/manager.internal.example.com.crt;
ssl_certificate_key /etc/nginx/certs/manager.internal.example.com.key;
proxy_pass wukongim_manager;
proxy_connect_timeout 5s;
proxy_timeout 1h;
}
}
```
Replace the domain and certificate paths, then publish the same Nginx address from every WuKongIM node:
```toml
[api]
external_tcp_addr = "im.example.com:5100"
external_wss_addr = "wss://im.example.com"
```
Point `im.example.com` to the client entry. Resolve `manager.internal.example.com` only through private DNS to the Nginx management address `10.0.0.10`. Administrators connect over the management network or VPN; node port `5301` allows only this proxy and authorized management sources, never public access. Replace the management address and certificate paths, then run `sudo nginx -t && sudo systemctl reload nginx`. Open-source Nginx only skips a node temporarily after connection failures; it does not actively check `/readyz`. Production automation must remove a node from `upstream` while that node is not ready. The example reserves port `443` for WuKongIM. One Nginx server is also a single point of failure, so use two Nginx servers behind one entry address or use a managed load balancer in production.
After the cluster contains data, do not directly change the cluster ID, node IDs, `hash_slot_count`, or replica counts. Read [Scaling](/en/server/operations/scaling) before adding or removing nodes.
# Backup & Restore (/en/server/operations/backup-and-restore)
Open **Manager → Cluster → Backups**. No `wukongim.toml` change is needed.
Permissions: backup `cluster.backup:w`; restore explicit `cluster.restore:w`. Archives are unencrypted; enable storage-side encryption.
## Backup exercise [#backup-exercise]
Keep automatic backup off until verification succeeds.
| Action | Expected result |
| ---------------------------- | ----------------------------------------------------- |
| Save settings → Test storage | Settings saved; storage **Verified** |
| Back up now | `256/256 Hash Slots`; recent task **Succeeded** |
| Verify → Hold | Archive `healthy`; **Archive verification succeeded** |
Every node's `data_dir/backup-repository` must point to the same shared storage. Without shared storage, choose OSS, COS, or S3.
Example settings and complete backup actions
Leave **Enable automatic backup** off and enter these example values:
| Setting | Example value |
| ---------------- | ------------------- |
| Backup frequency | Every day at 01:00 |
| Time zone | `Asia/Shanghai` |
| Storage | Shared file storage |
| Archives to keep | `7` |
| Workers per node | `1` |
| Timeout | `12` hours |
`data_dir/backup-repository` on every node must point to the same shared storage. Without shared storage, select OSS, COS, or S3.
1. Select **Save settings** and wait for **Backup settings saved**.
2. Select **Test storage** and wait for **Storage verification: Verified**.
3. Select **Back up now** and wait for `256/256 Hash Slots` under **Current task**.
4. Confirm **Succeeded** under **Recent tasks**.
5. Select **Verify** next to the archive, then select **Hold** after verification succeeds.
**Done when:** the archive is `healthy` and the page says **Archive verification succeeded**. Enable and save **Automatic backup** when you want scheduled runs.
## Restore exercise [#restore-exercise]
Messages and changes after the archive's completion time will be lost, and clients will disconnect. Use a maintenance window.
**Before starting:** Every `/readyz` returns 200, no other admin task runs, and the archive is freshly verified. Per-node free space: **current data + twice the archive's logical size + 1 GiB**.
Check the cluster, archive time, and size; confirm with the current administrator password and `RESTORE `. Keep nodes running; make no other changes.
**Before traffic:** The task succeeds, every `/readyz` returns 200, and history, sign-in, send/receive, reconnect, webhooks, and plugins pass.
Restore only to the archive's original cluster, never into a new cluster or as a v2 → v3 migration.
**On failure:** Preserve task/errors; do not repeat restore. See [Troubleshooting](/en/server/operations/troubleshooting#maintenance).
Complete restore actions and permission requirements
Messages and changes after the archive's completion time will be lost, and clients will disconnect. Use a maintenance window.
Before starting, confirm that every `/readyz` returns 200, no other administrative task is running, the archive has just passed verification, and each node has free space for current data + twice the archive's logical size + 1 GiB.
1. Select **Restore** next to the target archive.
2. Check the cluster, archive time, and size shown on the page.
3. Enter the current administrator password.
4. Enter `RESTORE ` as shown, then select **Start restore**.
5. Wait for the task to finish. Do not stop nodes or perform another change.
6. Sign in to Manager again, confirm **Recent tasks: Succeeded**, and check that every `/readyz` returns 200.
7. Check history, then test sign-in, message send and receive, reconnect, webhooks, and plugins. Restore traffic only after every check passes.
An archive restores only to the same cluster that created it. It is not a new-cluster import or v2-to-v3 migration tool.
On failure, preserve the task and error details instead of repeatedly starting restore. Backup requires `cluster.backup:w`; restore requires explicit `cluster.restore:w`. WuKongIM does not encrypt archive contents, so enable storage-side encryption.
# Health & Monitoring (/en/server/operations/health-and-monitoring)
Goal: check readiness on every node, then verify product behavior and monitoring separately.
## 1. Check readiness on every node [#1-check-readiness-on-every-node]
Use the actual API address from the management network or a protected loopback entry:
```bash
curl -sS -i http://127.0.0.1:5001/readyz
```
| Result | Next action |
| ------------------------- | --------------------------------------------------------------------------------------------------------- |
| HTTP `200`, `ready: true` | Use as a traffic-admission gate, then verify product messaging |
| HTTP `503` | Hold traffic to this node; save the full response and `reason` |
| Connection failure | Check the entry and network through [Troubleshooting](/en/server/operations/troubleshooting#connectivity) |
**`/healthz` proves only process liveness.** During restore or maintenance it may succeed while `/readyz` fails; that does not permit restoring product traffic.
Complete liveness, readiness, and metrics checks
```bash
curl -sS -i http://127.0.0.1:5001/healthz
curl -sS -i http://127.0.0.1:5001/readyz
curl -sS http://127.0.0.1:5001/metrics
```
Replace `127.0.0.1:5001` with the node's real API address. The third endpoint may be unavailable when Prometheus metrics are disabled.
| Endpoint | Question it answers | Correct use |
| ---------- | -------------------------------------------------------- | ---------------------------------------------------- |
| `/healthz` | Is the WuKongIM process alive? | Process liveness checks and automatic restart |
| `/readyz` | Can this node accept product traffic now? | Load balancing, rollout gates, and restoring traffic |
| `/metrics` | How are errors, latency, queues, and resources changing? | Prometheus collection, dashboards, and alerts |
Remember one rule: **a successful `/healthz` only means that the process is alive; only a successful `/readyz` means that the node can receive product traffic.** A failed `/readyz` returns HTTP 503 and a reason. Save the complete response.
During restore or other maintenance, `/healthz`, monitoring, and diagnostics may still work while `/readyz` remains failed. Do not restore product traffic just because the process is alive.
## 2. Check node state and product behavior [#2-check-node-state-and-product-behavior]
In [Manager](/en/server/operations/manager), inspect health, readiness, and data freshness. Then verify client connection, message sending, independent receipt, and history recovery.
**Check:** Inspect every node in a multi-node cluster. Preserve stale or conflicting state and pause changes. One readiness success does not verify messaging and recovery.
① Live-node count. ② Health, readiness, and data freshness. This reuses the recorded-version single-node cluster capture to show where to look; actual admission still checks `/readyz` per node. Click for the original.
## 3. Verify collection and alerts [#3-verify-collection-and-alerts]
Enable metrics, then have Prometheus scrape every node's `/metrics`. Check `up{job="wukongim"}=1` before comparing trends, alerts, and logs over the same interval. Use your own job name and cluster label.
**Successful collection does not prove readiness.** An empty result may mean an unconfigured target, disabled metrics, or an event that has not occurred; do not treat it as a healthy zero.
Cover connections and errors, queues and delivery, cluster state, and machine resources. Compare per-node maximums and high percentiles; assign an alert responder.
Four monitoring groups: signals and problems
Start with these four groups. You do not need a complicated dashboard on day one.
| Group | Signals and problems |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Traffic and connections | **Watch:** Connections, send errors, request rate, latency, reconnects **Problem:** Users cannot connect or messages become slow |
| Queues and delivery | **Watch:** Queue depth, rejection, drops, retries, webhook/plugin failures **Problem:** Messages back up or downstream delivery fails |
| Cluster state | **Watch:** Controller, Slot leaders, replicas, ISR, and tasks **Problem:** The process is running but cluster state is unhealthy |
| Machine resources | **Watch:** CPU, memory, goroutines, file descriptors, disk, and network **Problem:** Resource exhaustion makes the service unstable |
Manager Realtime and Top are useful for a quick view of one node. Prometheus shows trends over time. They complement one another, but neither replaces `/readyz`.
Prometheus: download, merge, and validate
Download [prometheus.yml](/examples/monitoring/prometheus.yml) and [alerts.yml](/examples/monitoring/alerts.yml) into one directory. Replace the three private node endpoints, keep one target for a single-node cluster, and set a unique `cluster` label. Enable node metrics through [Observability](/en/server/configuration/observability).
On the Prometheus host, validate the files before loading them through your existing service management process:
```bash
promtool check config prometheus.yml
promtool check rules alerts.yml
```
Confirm every target is UP in Prometheus Targets. For an existing configuration, merge only the relevant `scrape_configs` and `rule_files`; do not overwrite other jobs or scrape the same node twice. Configure notification receivers through your existing Prometheus/Alertmanager setup.
Four PromQL queries: labels, units, and interpretation
These queries use the sample's `job="wukongim"` and scrape-added `cluster` label; `node_id` comes from each node. Replace the selector if your job name differs. Check `up{job="wukongim"}` first: `0` means collection failed, while `1` only means a successful scrape, not successful `/readyz`. An empty result can mean an unconfigured target, disabled metrics, or an event that has not occurred; do not automatically interpret it as a healthy zero.
### Connections: compare nodes [#connections-compare-nodes]
```text title="PromQL"
sum by (cluster, node_id) (wukongim_gateway_connections_active{job="wukongim"})
```
The unit is connections, not distinct users. A skewed node suggests checking routing and connection distribution. For a sudden drop, compare gateway errors and client reconnects over the same interval.
### Send failures: inspect reasons [#send-failures-inspect-reasons]
```text title="PromQL"
sum by (cluster, node_id, reason) (increase(wukongim_gateway_sendacks_total{job="wukongim",reason!="success"}[5m]))
```
Shows unsuccessful SENDACKs emitted in the last 5 minutes; it does not count all client timeouts. For `auth_fail`, check UID, device category, and token. For other reasons, consult [Reason Code](/en/api/dictionaries/reason-codes) and the relevant node logs. Metric labels use names; protocol results use numeric codes.
### Slow appends: inspect per-node P99 [#slow-appends-inspect-per-node-p99]
```text title="PromQL"
histogram_quantile(0.99,
sum by (cluster, node_id, le) (
rate(wukongim_message_append_duration_seconds_bucket{job="wukongim"}[5m])
)
)
```
Values are seconds for server-side message append latency, not end-to-end receive latency. If P99 rises, compare disk IO, replication latency, and traffic on that node. Sparse traffic does not provide enough samples for a capacity conclusion.
### Delivery backlog: inspect queued batches [#delivery-backlog-inspect-queued-batches]
```text title="PromQL"
max by (cluster, node_id) (wukongim_delivery_recipient_worker_queue_depth{job="wukongim"})
```
The unit is queued delivery batches, not unread messages or undelivered users. For sustained growth, check delivery workers, downstream nodes, and connection writes before changing capacity.
The downloadable rules cover scrape failures, unsuccessful SENDACKs, and sustained delivery backlogs. The `2m`, `10m`, and `80%` thresholds are starter examples to tune against your baseline; they do not cover all readiness, storage, or cluster failures.
Alerts: complete starter list and threshold principles
* `/readyz` keeps returning 503 or repeatedly changes state.
* Send errors or latency keep rising.
* A queue keeps growing, or rejection, drops, or delivery failures appear.
* Disk space is low, or memory, file descriptors, or goroutines keep growing.
* Controller, Slot leader, replica, or ISR state is unhealthy.
* Monitoring collection stops, so cluster state becomes unknown.
Do not copy alert numbers from another environment. Observe the normal range for this version under a real product peak, then set thresholds. Large groups, high message rates, and many connections can create one-node or one-channel hot spots, so inspect maximums and high-percentile latency as well as averages.
## What to do after an alert [#what-to-do-after-an-alert]
Record time, impact, and the complete `/readyz` response. Align metrics, Manager, and logs; continue through [Troubleshooting](/en/server/operations/troubleshooting) when needed.
**Alerts must not automatically move leaders, delete nodes, or change topology.** Settings are in [Logs & Observability](/en/server/configuration/observability).
# Operations (/en/server/operations)
Start with three outcomes: nodes can accept traffic, someone responds to alerts, and data has a verified backup.
## Do these three things first [#do-these-three-things-first]
1. Check `/readyz` on every node, then verify product messaging and recovery.
2. Alert on readiness, errors, queues, and disk; assign a responder.
3. Configure backups in Manager, test storage, and confirm at least one complete, verified backup.
## What do you need to do? [#what-do-you-need-to-do]
A single-node cluster still follows Manager, Controller, and Slot procedures. Do not bypass them by editing data manually. The 256 physical hash slots remain fixed when nodes change.
## Use these five steps for every change [#use-these-five-steps-for-every-change]
What each step does and when to continue
1. **Record the current state**: Save versions, configuration, node state, `/readyz`, and key metrics. Continue when: You know what normal looked like before the change.
2. **Prepare a way back**: Record the rollback method, owner, and stop conditions. Continue when: Everyone knows who stops the change and how.
3. **Make a small change**: Change one thing at a time; advance one node or a small task batch. Continue when: The current step has finished.
4. **Test the product**: Test sign-in or reconnect, message send and receive, and history. Continue when: Product checks pass and nodes are ready again.
5. **Save the result**: Keep task results, errors, metrics, and the final node list. Continue when: Someone can later see exactly what happened.
## Stop if any of these happen [#stop-if-any-of-these-happen]
* `/readyz` returns 503, or readiness repeatedly changes between success and failure.
* Manager shows missing, stale, or conflicting node, Controller, Slot, Channel, or task state.
* A previous scaling, restore, or upgrade task has not finished.
* A production change has no rollback method, or no backup has passed verification.
* The release notes do not explicitly say whether the current and target versions may run together.
Stop and preserve the current evidence, then follow [Troubleshooting](/en/server/operations/troubleshooting). See [Server Tools](/en/server/tools) when you need a command-line tool.
# Manager (/en/server/operations/manager)
Manager shows cluster state, tasks, and backups and runs administrative actions. Start with a read-only account and these three steps.
## 1. Sign in with a manager account [#1-sign-in-with-a-manager-account]
The default local address is `http://127.0.0.1:5301`. For remote access, use the SSH tunnel in [Linux deployment](/en/server/deployment/linux), or an HTTPS proxy through a management network / VPN. Accounts come from `manager.users`.
① Enter the configured manager username and password. ② Select Sign in. This capture uses a read-only test account on an isolated cluster.
In production, enable `manager.auth_on`, replace default credentials and `manager.jwt_secret`, and keep node port `5301` off the public network. `manager.auth_on=false` is not read-only mode; SSH / VPN / HTTPS do not replace account permissions.
## 2. Check nodes [#2-check-nodes]
Open **Nodes**. Check the alive count, then readiness and freshness in the health column. Refresh and investigate stale state before continuing with administrative actions.
① Alive node count. ② Node health and readiness. This is a local single-node cluster example; check each node in a multi-node cluster.
## 3. Inspect tasks [#3-inspect-tasks]
Open **Tasks** and compare Running, Failed, and Completed. Filter by kind, status, or Slot ID and inspect the task timeline when investigating.
① Running. ② Failed. ③ Completed tasks in retained history. Counts change as the cluster runs; these are not fixed acceptance values.
## What to check elsewhere [#what-to-check-elsewhere]
| Page | Focus |
| --------------- | -------------------------------------------------------------------------------- |
| Live Monitor | CPU, memory, disk, connections, and queues; first confirm telemetry is available |
| Slots | Logical Slot Leader, replicas, ISR, and hash-slot ranges |
| Channel Cluster | Messaging runtime membership, Leader, and backlogs |
| Backups | Storage tests, archive completeness, and verification |
The Slot list shows **logical Slots**, initialized by `initial_slot_count`. The default **256 Hash Slots** form a separate fixed keyspace. Here, eight logical Slots each hold 32 Hash Slots. See [Slots and Hash Slots](/en/server/architecture/slots).
Account permissions and access paths
| Work | Common permissions |
| -------------------------------------- | --------------------------------------- |
| View nodes and scaling state | `cluster.node:r` |
| Join, activate, drain, or remove nodes | `cluster.node:w` |
| View or operate Slots | `cluster.slot:r` / `cluster.slot:w` |
| View or change backups | `cluster.backup:r` / `cluster.backup:w` |
| Restore data | Explicit `cluster.restore:w` |
Separate read-only, node-management, backup, and restore permissions instead of granting permanent full access.
Bind Manager to loopback or a management network. Keep the proxy-to-node connection on a controlled network, with the firewall allowing only the management proxy and authorized operator sources.
When you need to perform an administrative action
1. Record the operator, target, current state, and expected result.
2. Confirm `/readyz` passes and no conflicting task is still running.
3. Run any Plan or Preview first and read the blocking reasons.
4. Perform one step, wait for completion, and refresh authoritative state.
5. Verify message send, receive, and reconnect before continuing.
A Leader is the node responsible for the data, a replica is another copy, and ISR is the set keeping up. New operators should check health before making manual adjustments.
Remove a node only after authoritative state reports `safe_to_remove=true`. Restore requires full verification, password reauthentication, and a confirmation phrase. Stop when state is missing, stale, or conflicting.
Manager manages WuKongIM cluster state. Handle cloud-server shutdown, Kubernetes instance counts, and startup configuration updates separately. Continue with [Health & Monitoring](/en/server/operations/health-and-monitoring).
# Scaling (/en/server/operations/scaling)
This example starts with nodes `1–3`, adds node `4` at `10.0.0.14`, and then removes it. Open **Manager → Cluster → Nodes**.
Requires `cluster.node:w`. The cluster keeps 256 physical hash slots when nodes change.
## Scale-out: add node 4 [#scale-out-add-node-4]
### 1. Configure and start the new node [#1-configure-and-start-the-new-node]
Match `cluster.id`, replica counts, and `join_token` to the existing cluster. `/healthz` checks process liveness; it does not admit traffic.
Full configuration and startup commands
Copy an existing node's configuration and keep unrelated settings unchanged. Update `[node]`, remove `nodes` from `[cluster]`, and use:
```toml
[node]
id = 4
data_dir = "/var/lib/wukongim"
[cluster]
id = "prod-im-a"
listen_addr = "0.0.0.0:7000"
advertise_addr = "10.0.0.14:7000"
seeds = ["10.0.0.11:7000"]
join_token = "the-same-secret-as-existing-nodes"
hash_slot_count = 256
slot_replica_n = 3
channel_replica_n = 3
```
Match the existing cluster's `cluster.id`, replica counts, and `join_token`. Start the process and check liveness:
```bash
sudo systemctl enable --now wukongim
curl --fail http://10.0.0.14:5001/healthz
```
### 2. Activate it in Manager [#2-activate-it-in-manager]
Wait for `joining`, select **Activate node**, then confirm `active`.
Complete activation actions
1. Wait for node 4 to show `joining`.
2. Open node 4 and select **Activate node**.
3. Wait for status `active`.
### 3. Move data in [#3-move-data-in]
Set **Max slot moves** to `1`. Plan and start onboarding; advance again only after active tasks reach 0, until no new task is created.
Complete onboarding actions
1. Set **Max slot moves** to `1`.
2. Select **Plan slot onboarding** and confirm that node 4 is the target.
3. Select **Start onboarding**. When active tasks reach 0, select **Advance onboarding**.
4. Repeat until no new task is created.
### 4. Verify [#4-verify]
Check `/readyz` on all four nodes and test message send and receive. Only then add `10.0.0.14:5100` and `10.0.0.14:5200` to the load balancer.
Readiness commands and traffic admission
```bash
curl --fail http://10.0.0.11:5001/readyz
curl --fail http://10.0.0.12:5001/readyz
curl --fail http://10.0.0.13:5001/readyz
curl --fail http://10.0.0.14:5001/readyz
```
After all four requests pass and messages work, add `10.0.0.14:5100` and `10.0.0.14:5200` to the load balancer.
## Scale-in: remove node 4 [#scale-in-remove-node-4]
Remove and stop it only after Manager shows **Safe to remove: Yes** (`safe_to_remove=true`). Keep the node running when blocked.
| Phase | Required result |
| ------------------------------------------------- | ------------------------------------------------------------------------------ |
| Remove traffic → Mark leaving → Enable drain mode | Other nodes carry the load; active, closing, and pending connections are all 0 |
| Plan → Advance scale-in → Refresh | Slot, Channel, and active task counts are 0; **Safe to remove: Yes** |
Set **Scale-in max slot moves** to `1`; advance again after active tasks reach 0. Remove the node only after the gate passes, then stop its service.
**Done when:** Remaining nodes pass `/readyz` and messaging works. Retain node 4's data until the product is stable, then follow your retention policy.
Complete scale-in actions and final stop command
Remove and stop the server only after Manager shows **Safe to remove: Yes**.
1. Remove node 4 from the load balancer and confirm that the other nodes can carry the traffic.
2. Open node 4, select **Mark leaving**, and wait for status `leaving`.
3. Select **Enable drain mode**. Wait for active, closing, and pending connection counts to reach 0.
4. Set **Scale-in max slot moves** to `1`, then select **Plan scale-in** → **Advance scale-in**. Advance again after active tasks reach 0.
5. Select **Refresh scale-in status**. When Slot, Channel, and active task counts are 0 and **Safe to remove: Yes** appears, select **Remove node**.
6. Finally, run on node 4:
```bash
sudo systemctl disable --now wukongim
```
Check `/readyz` on the remaining nodes and test messages again. Keep node 4's data directory until the product is stable, then handle it under your data-retention process.
If an action is disabled or Manager shows a blocking reason, stop and keep the node running, then follow [Troubleshooting](/en/server/operations/troubleshooting). This procedure requires `cluster.node:w`.
**Stop if blocked:** Disabled actions or blocking reasons mean keep the node running and preserve evidence. Continue through [Troubleshooting](/en/server/operations/troubleshooting#cluster-state).
# Troubleshooting (/en/server/operations/troubleshooting)
Record time, impact, and recent changes, then choose a symptom.
Pause releases and topology changes if node, Controller, Slot, Channel, or task state is missing, stale, or conflicting. Preserve evidence; avoid bulk restarts, leader movement, and data deletion.
## The first ten minutes [#the-first-ten-minutes]
Complete collection order and read-only commands
1. **Pause other changes**: pause releases, scaling, restore, and concurrent cluster operations. Record every active task.
2. **Record the impact**: note the start time and affected users, Channels, nodes, and entry points (TCP, WebSocket, or HTTP).
3. **Check liveness and readiness**: save the status code and full response from `/healthz` and `/readyz`; the `/readyz` failure reason matters most.
4. **Open Manager**: inspect nodes, Controller, Slots, Channels, and tasks. Mark anything unhealthy or no longer updating.
5. **Align the time range**: compare metrics, error logs, and recent release or configuration changes from the same period.
```bash
curl -sS -i http://127.0.0.1:5001/healthz
curl -sS -i http://127.0.0.1:5001/readyz
wkcli top --server http://127.0.0.1:5001 --once --json
```
Replace the address with the real API address. `0.0.0.0` is a listen setting, not a server address that clients can use.
For metric names, copyable PromQL, and result interpretation, see [Health & Monitoring](/en/server/operations/health-and-monitoring). Check collection health before comparing data from the same node and time interval.
## Continue from the symptom [#continue-from-the-symptom]
### `/healthz` succeeds but `/readyz` returns 503 [#healthz-succeeds-but-readyz-returns-503]
Save the full `/readyz` response and `reason`. Inspect maintenance, Controller, Slots, and active tasks in Manager, then compare other nodes.
**Check:** Identify the specific readiness failure. Do not restore traffic before the restore, scaling, or upgrade stop conditions are cleared.
Readiness: complete check list
1. Save the complete `/readyz` response and `reason`.
2. In Manager, check maintenance state, Controller, Slots, and active restore or control tasks.
3. Compare time, version, configuration, and readiness with other nodes.
Do not return traffic to this node or bypass safety checks in restore, scaling, or upgrade procedures.
### Clients cannot connect or keep reconnecting [#clients-cannot-connect-or-keep-reconnecting]
Confirm the client protocol and advertised address. Check DNS, load balancer, TLS, firewall, and gateway logs over the same interval.
**Check:** Narrow the failure to an entry, node, or connection step and retain evidence. Do not disconnect or restart in bulk without evidence.
Connections: complete check list
1. Identify whether the user connects through TCP, WebSocket, or HTTP. Do not use the Manager address as a client address.
2. Check the advertised address, DNS, load balancer, TLS, and firewall.
3. Compare connection counts, connection errors, file descriptors, memory, and network by node. Read gateway logs for the same user and time window.
Do not disconnect sessions in bulk or restart every node without evidence.
### Messages fail, slow down, or back up [#messages-fail-slow-down-or-back-up]
Inspect send errors and high-percentile latency, then compare node queues. Use an exact Channel lookup when its ID is known.
**Check:** Identify the node or Channel hot spot. Verify commit, peer receipt, and history recovery separately; averages cannot replace high percentiles.
Messaging: complete check list
1. Check send error rate and high-percentile latency, then see whether Channel, delivery, or push queues keep growing.
2. Compare nodes to find a single-machine or large-group hot spot.
3. When you know the Channel ID, use an exact lookup instead of listing every Channel.
4. Use pprof for a short, bounded window only after metrics and logs point to CPU, memory, or goroutines.
See [Message Flow](/en/server/architecture/message-flow) for the full path. With large groups and high message rates, averages can look normal while maximums and high percentiles are unhealthy.
### Controller, Slot, or node state is unhealthy [#controller-slot-or-node-state-is-unhealthy]
Save state timestamps, control revision, `blocked_reasons`, tasks, and Slot evidence. Pause changes for missing, stale, or conflicting state.
**Check:** A diagnostic recommendation is not authorization to write. Node removal still requires `safe_to_remove=true`.
Cluster state: read-only diagnostic commands
```bash
wkcli node ls --context production
wkcli node diagnose 4 --context production --json
```
Save state timestamps, control revision, `blocked_reasons`, tasks, and Slot evidence from the output. A diagnostic recommendation is not permission to perform a write. Node removal must still wait for `safe_to_remove=true`. Follow [Scaling](/en/server/operations/scaling) for the procedure.
### Disk is low or data looks inconsistent [#disk-is-low-or-data-looks-inconsistent]
Record free disk, IO, errors, node ID, and data paths. Do not delete data files or unknown logs to make space.
**Check:** `wkcli db` reads only stopped nodes, snapshots, or copied stores. An `import` must write to an explicitly offline target; it does not replace backup and restore.
Disk and data: offline inspection boundaries
First record free disk, IO latency, errors, node ID, data paths, and configuration. Do not make space by deleting data files or unknown logs.
`wkcli db` can inspect only a stopped node, filesystem snapshot, or copied data directory. Start with read-only `query`, `diff`, or `export`. `import` writes data, must target an explicitly offline store, and does not replace Manager backup and restore.
### Backup, restore, or upgrade failed [#backup-restore-or-upgrade-failed]
Preserve Manager tasks and audits, archive inventory, physical Slot verification, and version digests. Follow the relevant task stop conditions.
**Check:** A live `/healthz` during restore does not permit traffic. Mixed-version operation requires explicit release-note support.
Maintenance failures: archives and stop conditions
Save Manager tasks and audits, archive inventory, verification for all 256 physical Slots, and program versions and digests. Do not return traffic because `/healthz` succeeds during restore, and do not assume arbitrary versions may remain mixed after a failed upgrade. Return to the stop conditions in [Backup & Restore](/en/server/operations/backup-and-restore) or [Upgrade & Migration](/en/server/operations/upgrade-and-migration).
## Use tools in this order [#use-tools-in-this-order]
Start with `/readyz`, Manager, and metrics and logs over the same interval. Narrow to one node or an exact Slot, Channel, or task. Time-box and target pprof, then disable Debug; use `wkcli bench` only in an isolated test cluster.
Complete tool order and usage boundaries
1. Start with `/readyz` and Manager; they are the lowest-cost checks.
2. Read Prometheus metrics and error logs from the same time range.
3. Use Top or `wkcli top` for one node's current resources and short history.
4. Use [Diagnostics](/en/server/tools/diagnostics) only after the problem is narrowed to an exact node, Slot, Channel, or task.
5. Time-box and target pprof, then disable Debug access when finished.
6. `wkcli bench` creates real traffic and data. Reproduce only in an isolated test cluster; never run an unbounded load against production.
Stop increasing diagnostic cost when an earlier step already explains the problem.
## What to send when asking for help [#what-to-send-when-asking-for-help]
Provide time, impact, versions and redacted configuration, full readiness responses, and matching metrics with a short log excerpt. Remove passwords, full tokens, and message bodies before sharing.
Help request: complete evidence list
* Incident start time, impact, and recent changes.
* Node inventory, versions, cluster identity, and relevant redacted configuration.
* Complete `/healthz` and `/readyz` responses and Manager state.
* Metric screenshots or exports and a small relevant log excerpt from the same time range.
* Relevant node, Slot, Channel, task, or trace IDs.
* Checks already performed, their results, and high-risk actions deliberately not performed.
Do not send passwords, complete tokens, user message bodies, or a full unreviewed configuration file.
Troubleshooting is complete only after the product works again, queues are controlled, nodes pass `/readyz`, monitoring has recovered, and temporary Debug access, sampling, and test traffic have been removed.
**Completion criteria:** The product works, queues are controlled, nodes pass `/readyz`, monitoring has recovered, and temporary Debug access, sampling, and test traffic are removed.
# Upgrade & Migration (/en/server/operations/upgrade-and-migration)
Before upgrading, answer one question: **do the target release notes explicitly allow the target and current versions to run together?** Do not guess based on cluster size or past experience.
**Moving from v2 to v3?** Use the [offline migration procedure](/en/server/operations/v2-to-v3-migration), not the upgrade flows below.
## Choose the upgrade method [#choose-the-upgrade-method]
Rolling upgrades require explicit release-note support, including order and time limit. Silent, unclear, or incompatible notes mean a stopped upgrade, or pause and ask the release owner.
Complete version decision table and compatibility rule
| Situation | Method |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| The release notes explicitly allow these versions to mix and give an order and time limit | A rolling upgrade may update one node at a time |
| The release notes are silent, unclear, or say they are incompatible | Use a stopped maintenance window and keep every node on the same version |
| Moving from v2 to v3 | Treat it as a separate migration project; do not copy data directories or use the normal upgrade steps |
Use a rolling upgrade only when the target release documentation explicitly confirms compatibility. Without that evidence, use a stopped upgrade or pause and ask the release owner.
## Before you start [#before-you-start]
First confirm three things:
1. Exact versions and digests, compatibility, order, mixed-version deadline, and rollback boundary are known.
2. Storage is tested, a complete archive is verified, and restore is rehearsed.
3. Readiness, metrics, and product checks have a recorded baseline.
**Unknown checks mean pause.** Do not begin a rolling upgrade when any preflight item is unclear.
Complete preflight check list
* Record the current and target versions and the digest of every package or image.
* Read the target release's compatibility notes for protocols, storage, configuration, Controller, Slots, and Channels.
* Write down the upgrade order, maximum mixed-version time, stop conditions, and owner.
* Confirm whether downgrade is supported and after which step a direct binary rollback becomes unsafe.
* Confirm that backup storage passed its test, one complete archive passed verification, and restore was rehearsed.
* Save a pre-change baseline for `/readyz`, errors, latency, connections, queues, disk, and cluster state.
* Test message send, receive, reconnect, history, and downstream integrations.
Do not begin a rolling upgrade if any item is unknown.
## Rolling upgrade [#rolling-upgrade]
**Only with explicit compatibility:** One node at a time; keep its ID and data directory unchanged and obey the release-specific order and mixed-version deadline.
Pause scaling, backup/restore, and unrelated configuration changes first.
Confirm `/readyz` is 200 and Controller, Slots, Channels, errors, and queues are healthy. Then test messaging, reconnect, history, and downstream integrations with a small amount of traffic. Advance only after this node is stable. Keep headroom for reconnects and warm-up; after completion, observe a full product peak and save results.
Complete rolling upgrade sequence
Use this only when the release notes explicitly allow mixed versions:
1. Pause scaling, backup/restore, and unrelated configuration changes.
2. Remove one non-critical node from the load balancer and wait for connections and tasks to drain.
3. Restart it with the target version and validated configuration. Keep its node ID and data directory unchanged.
4. Wait for `/readyz` to return 200. Confirm that Controller, Slots, Channels, errors, and queues are healthy.
5. Send a small amount of traffic to the node and test message send, receive, reconnect, history, and downstream integrations.
6. Upgrade the next node only after this one is stable. Follow the release-specific node order and mixed-version deadline.
7. After all nodes are upgraded, observe at least one complete product peak and save the results.
Large groups, high message rates, and many online users amplify reconnect and cache warm-up pressure. Keep capacity headroom during the upgrade.
## Stopped maintenance upgrade [#stopped-maintenance-upgrade]
Complete backup verification, rollback rehearsal, and downtime communication first. Drain writes and downstream delivery. Follow the release's stop/start order; do not stop every Controller voter together while coordination is needed.
**Before traffic:** Every node passes `/readyz`; nodes, Controller, and logical Slots are stable; product tests pass. Restore traffic gradually and stop on unknown state or new errors.
Complete stopped upgrade sequence
Use this when the release notes do not explicitly allow mixed versions:
1. Complete backup verification, rollback rehearsal, and downtime communication.
2. Stop product traffic and wait for writes and downstream delivery to drain.
3. Stop nodes in the order required by the target release. Do not stop every Controller voter together while coordination is still needed.
4. Replace the program and configuration on every node. Start them according to the release instructions only after versions are consistent.
5. Wait for all nodes, Controller, and all logical Slots to stabilize and for `/readyz` to return 200 on every node.
6. Run product tests and then restore traffic gradually. Stop the cutover on unknown state or new errors.
## When rollback is safe [#when-rollback-is-safe]
A direct binary rollback needs **explicit downgrade support and no data that the old version cannot read**. Configuration, metadata, and storage can create irreversible boundaries.
**Past the boundary or unsure:** Keep traffic closed and preserve logs/data. Use release-specific recovery or the rehearsed backup restore; do not blindly install the old binary.
Complete rollback boundary explanation
A direct binary rollback is safe only when the release notes explicitly support downgrade and the new version has not written data that the old version cannot understand. Configuration, metadata, and storage formats can all create an irreversible boundary.
After that boundary, do not blindly install the old version. Keep traffic closed, preserve logs and data, and follow the release-specific recovery procedure or the rehearsed backup restore.
## Moving from v2 to v3 [#moving-from-v2-to-v3]
`wkcli migrate` converts an original v2 stopped backup offline into a **new, empty v3 cluster**. Never open the old directories directly in v3. Follow [v2 → v3 Offline Migration](/en/server/operations/v2-to-v3-migration); after new production writes begin, a direct switch back to v2 is unsafe.
Complete migration scope and acceptance order
`wkcli migrate` converts complete stopped backups of original v2 into a new, empty v3 cluster offline. It does not require modifying or upgrading old v2. It is not an in-place upgrade, and v3 must never open the old data directories directly.
Follow [v2 → v3 Offline Migration](/en/server/operations/v2-to-v3-migration) to fix versions and the plan, run `prepare`, `export`, `import`, and independent `verify`, then complete runtime, client, and external integration acceptance. Renumbering requires handling client caches and cursors; after new production writes begin, a direct switch back to old v2 is unsafe.
# v2 → v3 Migration Reference (/en/server/operations/v2-to-v3-migration-reference)
Look up multi-node plans, compatibility policies, retries and cutover by task. For a first migration, follow the [walkthrough](/en/server/operations/v2-to-v3-migration).
Full migration scope and validated source baseline
Start with the [v2 → v3 migration walkthrough](/en/server/operations/v2-to-v3-migration). Use this reference for multi-node deployments, plugins, and troubleshooting. Review optional policies against your own business requirements.
This guide covers the general offline migration of an existing WuKongIM v2 deployment. Run the migration on one machine: **collect cold backups → fill in the plan → migrate and verify → deploy, test, and cut over**. You do not need to modify or upgrade v2: `prepare` reads stopped backups directly.
The validated source baseline is original `v2.2.5-20260422`, supporting single-node and multi-node clusters and node-count changes. Other v2 versions and custom builds require a compatibility assessment and cold-backup rehearsal; a v2 version label alone does not establish support. **All v2 nodes must be stopped, and v3 must be a new, empty cluster.** Never let v3 open a v2 data directory.
Version, capacity, and backup requirements
| Item | Requirement |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| Source version | Original `v2.2.5-20260422`, commit `a888f89533d0e7d1b2030e06504ca97f1ad891d4`; custom changes need separate assessment |
| Migration method | Offline, after stopping writes and all source nodes; no online incremental catch-up |
| Source and target | Single-node and multi-node clusters, including changes in node count; targets must be entirely new and empty |
| Runtime platforms | Source file locking supports Linux and macOS; plugin programs must also match the target OS and architecture |
| Cluster layout | Exactly 256 physical hash slots; the target plan specifies logical Slot Raft Group and replica counts |
| Tool and target versions | Build the tool and server from the same fixed, validated v3 source commit, or use a matching delivery package with verified digests |
The migration machine needs space for full cold backups, workspaces, the archive, every target replica, and snapshots taken before acceptance testing. Rehearse with representative data and record peak space and elapsed time. Use measurements from your own deployment to schedule the final outage, rather than another deployment’s backup size or timings.
Budget CPU and memory per host, including every colocated node, plugin subprocess, and other service. A rehearsal may share resources with a running v2 cluster, and the first search-index rebuild has different costs from steady operation. For Docker Compose, set per-service CPU, memory, and process limits from rehearsal measurements; observe peaks, OOM events, restarts, and API latency. Go's `GOMEMLIMIT` applies to each Go process and does not replace a container-wide memory limit. Do not copy another deployment's values. The complete functional and recovery acceptance checks must still pass under the selected limits.
If you set `GOMEMLIMIT`, budget for the measured live Go heap, runtime overhead, and request allocation headroom. A soft limit below the live heap can cause persistent garbage collection and API timeouts even before the container reaches its memory limit. Correlate CPU and heap profiles before adjusting caches or the soft limit, while retaining headroom for non-Go memory, plugins, and the host. See the [Go garbage collector guide](https://go.dev/doc/gc-guide).
Cold startup also restores snapshots and replays committed cluster logs. If logs and diagnostics show recovery progressing when startup readiness times out, first check resources, disks, and node communication, then size the wait from measured recovery time. Versions supporting this option expose `cluster.start_timeout` (`WK_CLUSTER_START_TIMEOUT`, default `30s`); verify version support and validate the configuration first. This extends only the startup wait and never relaxes quorum, committed-write, or placement checks. It cannot make a persistently unavailable cluster ready.
The migration archive preserves original business records, but not every original WAL byte. It cannot replace a full filesystem cold backup. Protect the credentials, messages, and plugin configuration it contains.
## 1. Collect backups and install the tools [#1-collect-backups-and-install-the-tools]
Original `v2.2.5-20260422` is the source baseline. Assess other versions, custom builds, storage formats, `Expire`, plugins and interfaces separately. `source_commit` only selects reader rules. Check the selected tool’s released capabilities and matching binary identity; an unpublished fix is not an installed-package capability.
Stop writes and all v2 nodes; use fresh empty targets. Rehearse with full backups to measure space, resources and duration. The archive cannot replace a full filesystem cold backup; online incremental catch-up is unsupported.
Full version, compatibility and matching-build rules
**Check the actual binaries, storage format, and plugins before downtime.** A Docker image tag need not match its binary version: development images may contain another commit or uncommitted changes. `source_commit` selects the reader schema; omitting or changing it cannot make an unsupported database compatible.
Record each source node’s actual binary version/commit, data directory, database shard count, node ID, storage format, plugins, and external interfaces. Image tags and defaults are clues; inspect the actual binary and mounted data. Read `marker.format-version.*` and `format_major_version` in `OPTIONS-*` without changing format markers.
Confirm each required capability against the selected tool’s release notes before running this guide. Basic migration, storage adapters, and optional transformations can be introduced in different versions; having `wkcli` installed does not imply support for every option.
| Check | Action before migration |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Source version and storage format | Released `v3.0.0-beta.13` reads Pebble formats up to 14; format 19 requires a tool that explicitly supports it. Readable storage must still pass field, index, and replica validation. |
| Message expiration | Preserve the original `Expire`, including nonzero values. `v3.0.0-beta.13` lacks complete native expiration propagation; these records require matching tools and runtimes whose release notes explicitly support preservation. Do not zero the field. |
| Plugins | Inventory each program’s version, size, digest, configuration, and methods. Use a supported compatibility profile and validate actual behavior; see “Plugin migration” below. |
| Webhooks | Verify the target protocol, endpoint, and connectivity. Do not place a gRPC address into HTTP configuration; verify event handling at the receiver. |
| Person whitelist | v2 `whitelistOffOfPerson=false` corresponds to v3 `message.person_whitelist_enabled=true`; do not blindly accept the v3 default. |
| Gateway | Check TCP, WebSocket, and HTTP forwarding. The current v3 gateway does not accept the prefix sent by `proxy_protocol on`; remove that old setting and run `nginx -t` first. |
| External data source | There is no direct equivalent of v2 `datasource.addr`. Validate imported membership/allowlists/denylists and subsequent changes through supported target APIs. |
Use the same matching version on every target node. If published packages lack a required capability, complete adaptation and release-version validation before scheduling the final outage. Do not assume an unpublished fix is present in an installed package. Once a newer runtime writes target data, follow its data-format rollback constraints instead of merely swapping in an older binary.
If v2 writes resume after a brief outage to take a rehearsal backup, that backup is for rehearsal only. Before the actual cutover, stop all write entrypoints again, take a new complete cold backup, and repeat migration and independent verification with fresh workspaces and target directories. Never route production traffic to a stale rehearsal generation.
Stop all business write entrypoints, let log application, topology changes, and notification queues drain, then shut down every v2 node normally and disable automatic restarts. Preserve complete node data directories, original binaries, configuration, and environment variables. **An existing complete stopped backup can be used directly; do not restart v2.**
Choose a migration machine with enough space. **Copy every v2 node's cold backup into a separate directory on that machine**, and compare file inventories and content checksums. Do not merge node directories. Run the migration commands once, on this machine only:
| Cold backup on each v2 server | Separate directory on the migration machine |
| ----------------------------- | ------------------------------------------- |
| v2-a, node 1001 | `/srv/v2-snapshots/node1001/data` |
| v2-b, node 1002 | `/srv/v2-snapshots/node1002/data` |
| v2-c, node 1003 | `/srv/v2-snapshots/node1003/data` |
Multiple servers: copy one source node
Run on the migration machine, replacing the SSH hostname, backup path, and node ID. Repeat for every other source node. Remote backups must be complete and immutable; receiving directories must belong exclusively to this attempt.
```sh
mkdir -p /srv/v2-snapshots/node1001/data
rsync -a --numeric-ids --partial root@v2-a:/srv/v2-cold-backup/data/ /srv/v2-snapshots/node1001/data/
rsync -anc --numeric-ids --delete --itemize-changes root@v2-a:/srv/v2-cold-backup/data/ /srv/v2-snapshots/node1001/data/
```
The trailing `/` on the source copies directory contents, including hidden files and the full hierarchy. After the first `rsync` succeeds, run the second to verify. Its `-n` means dry run; `--delete` only lists extra files and deletes nothing. Require exit code 0 and no differences before migration. Save the results and retain the original full backups.
Interrupted copies may resume from the same immutable backup, followed by full verification. Once `prepare` starts, do not sync more files into the collected sources.
Tool installation, directories and identity checks
Follow [wkcli installation](/en/server/tools/wkcli) to obtain matching `wkcli` and `wukongim` binaries (included in official packages since v3.0.0-beta.12). Verify the delivery digests, then prepare directories:
```sh
umask 077
mkdir -p /srv/wkmigrate/reports /srv/wkmigrate/work /srv/wkmigrate/targets
wkcli version --output json
wukongim version --output json
```
Confirm that both binaries have the same version, commit, and build source. Create only parent directories here. Do not precreate `work/prepare`, `work/import`, `work/verify`, `source-archive`, or target node directories.
## 2. Fill in one plan.json [#2-fill-in-one-planjson]
**Pass:** one plan includes every source; run centrally once. Never merge source directories or run separate per-server plans. All paths are absolute and must not contain one another; targets do not yet exist. Deploy at the same absolute paths. Use actual node IDs, final communication addresses, shard and replica counts.
`slot_count=12` is the example’s logical Slot Raft Group count, matching `initial_slot_count`; `hash_slot_count=256` counts physical hash slots. Specify both explicitly; replicas cannot exceed target nodes. Optional transformations require business approval and exact original-row/proof bindings. Never delete rows, clear fields or edit markers to force a pass.
Full plan, path and capability requirements
Save the example below as `/srv/wkmigrate/plan.json`. Adjust source node IDs, shard counts, target nodes, addresses, and creation time. For a single-node cluster, keep one target node and set both `replicas` and `channel_replicas` to `1`; still include every source node.
The base example explicitly includes `source_commit`, including compatibility with tools that require it. This value selects supported reader rules; it cannot declare arbitrary old data compatible. Use the optional policies below only when the selected tool’s release notes explicitly support them, and omit options you do not need.
**Every path in the plan is an absolute path on the migration machine.** `sources` points to collected backups; `target.nodes` points to directories that will be generated on that machine. `addr` is the final v3 cluster communication address. The tool does not access remote directories over SSH, and you must not run separate plans on individual source servers.
Expand and copy: three-node plan.json
```json
{
"version": 1,
"source_commit": "a888f89533d0e7d1b2030e06504ca97f1ad891d4",
"sources": [
{"node_id": 1001, "data_dir": "/srv/v2-snapshots/node1001/data", "shard_count": 8},
{"node_id": 1002, "data_dir": "/srv/v2-snapshots/node1002/data", "shard_count": 8},
{"node_id": 1003, "data_dir": "/srv/v2-snapshots/node1003/data", "shard_count": 8}
],
"target": {
"cluster_id": "migration-v3-new",
"created_at": "2026-09-09T00:00:00Z",
"slot_count": 12,
"hash_slot_count": 256,
"replicas": 3,
"channel_replicas": 3,
"nodes": [
{"node_id": 101, "addr": "10.20.0.11:7000", "data_dir": "/srv/wkmigrate/targets/node101"},
{"node_id": 102, "addr": "10.20.0.12:7000", "data_dir": "/srv/wkmigrate/targets/node102"},
{"node_id": 103, "addr": "10.20.0.13:7000", "data_dir": "/srv/wkmigrate/targets/node103"}
]
}
}
```
`source_commit` may be omitted. The tool selects its supported v2 reader schema and checks actual structures, fields, indexes, and business compatibility during reading; incompatible data still blocks completion. The `source_commit` in reports and archives identifies the reader schema revision, not an automatically detected source binary. Known different versions or custom modifications still require separate assessment.
Older tools, including v3.0.0-beta.12, still require `"source_commit": "a888f89533d0e7d1b2030e06504ca97f1ad891d4"` at the plan top level. New tools continue to accept that plan with the same migration identity. Explicitly specifying a different revision is still rejected. Set `shard_count` to the actual original business DB shard count. Target node IDs are 1–1023; replica counts cannot exceed the target node count. `slot_count` sets target logical Slot Raft Groups. The example uses the initialization sample of `12`, which must match `cluster.initial_slot_count` at startup. The migration plan still requires an explicit value; no default is filled in. `hash_slot_count` is the physical hash-slot count, fixed at `256`.
Source directories, workspaces, the archive, and target directories must not contain one another. Target `data_dir` directories must not exist yet, and must later be deployed at the same absolute paths on their respective servers.
Optional: plugin migration
Deployments without enabled plugins need no plugin fields. Otherwise, inventory each original program, configuration, method, and supported compatibility profile. A profile may bind an exact binary digest; do not copy another deployment’s digest/profile onto your program or remove registrations to bypass validation.
* `plugin_nodes` assigns a source’s settings to every target, including expansion or contraction.
* `plugin_configs` explicitly selects a shared configuration source for an individual plugin; other settings follow the node mapping.
* `plugin_artifacts` records each source program’s actual path, size, SHA-256, and supported profile. Its platform and architecture must match the target.
These fields extend the base plan. See [Plugin migration configuration](https://github.com/WuKongIM/WuKongIM/blob/main/cmd/wkcli/internal/migrate/README.md) for their full structure and each profile’s scope. Adapt unsupported plugins before proceeding, and retain complete original files and settings.
Complete offline `verify` before plugins first start. Search plugins may rebuild indexes from migrated history, requiring additional disk, CPU, and time; do not blindly copy indexes bound to old nodes or sequences. Validate historical search, new-message indexing, cross-node queries after writing to just one node, Leader changes, and full restarts. Readiness and a running plugin process do not replace these checks.
If a search index can be fully rebuilt from IM messages, you may instead omit its historical index and checkpoints, preserve the original backup, plugin program, and configuration, and use the plugin's supported empty-data-directory startup procedure. Record this choice and track historical search rebuilding separately from IM message migration acceptance. New-message indexing and plugin integration still require validation. Do not apply this option to irreplaceable plugin business data or remove plugin registrations or configuration stores to bypass checks.
If callers use `/plugins/:plugin_no/*path`, confirm that the selected target release exposes that HTTP route; `v3.0.0-beta.13` does not yet provide this compatibility entry. Starting a plugin alone is insufficient.
For multi-node acceptance, stop each node in turn and call plugin APIs through every surviving node, then restore the node and verify a full cluster restart. After a channel Leader changes, verify that plugin requests reach its current owner. Success through one node does not prove that other nodes have stopped forwarding to the old Leader. This check also applies when historical search indexes are excluded from migration.
By default, business compatibility is checked strictly; **deduplication and CMD/stream omissions are not enabled automatically**. If needed, read the policy details below and update the plan before migration.
Optional: deduplication, CMD/stream exclusions, and renumbering
The default checks business equivalence strictly. The following policies require explicit decisions; exceptions from one rehearsal must not be copied into another backup's plan without assessment.
| Data | Policy |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| Ordinary messages | Preserve MessageID, ClientMsgNo, payload, and native RedDot for retained messages; incompatible fields block completion |
| Users, devices, and permissions | Import original credentials, membership, and permissions; duplicate device credentials do not use the message “keep latest” rule |
| Conversations and read positions | Preserve equivalent state; map read and deletion positions when renumbering |
| Old management data | Archivable records remain in the checksummed source archive and are listed in the report |
| Plugins and external integrations | Require explicit programs, configuration, and compatibility mappings; archiving or disabling a plugin does not establish business compatibility |
If the business owner decides to omit CMD and entire stream messages, deduplicate, and renumber, merge these fields into the top level of the plan:
```json
{
"messages": {
"keep_latest_duplicates": true,
"exclude_cmd": true,
"exclude_streams": true,
"compact_sequences": true
},
"exclusions": {"legacy_stream_storage": true}
}
```
`legacy_stream_storage` **only excludes the old Stream and StreamMeta tables**. `exclude_streams` additionally excludes stream main messages from the message table and their explicitly associated event projections and cursors. Event identities shared with retained messages still block completion. `exclude_cmd` also omits old CMD conversations and sync positions. All excluded source rows remain in the archive.
“Latest” means the greatest **original MessageSeq within the same channel**. Keep the newer record for duplicate MessageIDs or the same channel with matching nonempty sender and nonempty ClientMsgNo. Empty senders or ClientMsgNo values skip ClientMsgNo deduplication; MessageID deduplication remains independent. Cross-channel MessageID conflicts and contradictory retention choices still block completion. Do not add physical replica counts across nodes to calculate business deletions.
After renumbering, retained messages become `1…N` in their original order. Read and deletion positions map to the number of retained messages at or before the old position; a channel with every message excluded has imported tail 0. Runtime recovery barriers also consume channel positions, so use the actual returned sequence and require new messages to be strictly beyond the current durable tail. Do not assume the first message after startup or restart is exactly `N+1`. Migration does not create placeholders or change v3 storage to fill gaps. Historical gaps without original-row evidence still block completion.
Optional: preserve conversations beyond the old list cap and recover conflicting states
The original `conversation.userMaxCount` caps the v2 list response; storage may contain more valid conversations. Keep the actual source value in `metadata.conversation_list_limit`. After an explicit business decision to preserve those durable conversations, enable `metadata.preserve_all_conversations: true` alongside `conversation_lookup: "v2_active_slot"`. The report records `users_over_original_limit` and `max_leader_chat_rows`. This does not authorize creating missing conversations or clearing unread state.
Conflicting list and unique-index states still fail by default. A reviewed `metadata.conversation_recoveries` entry binds `node_id`, `logical_key` (`IdentityKey(uid, channelID, channelType)`), `indexed_sha256` (SHA256 of `json.Marshal(Row)`), and `rows_sha256` (SHA256 of each original row's SHA256 plus a newline, ordered by physical ID). It selects one complete original indexed record, preserves its unread/read/deletion state, and then applies the approved message sequence mapping.
Missing or conflicting replicas require separate exact-group decisions. Each `metadata.conversation_replicas` entry binds `logical_key` and `copies_sha256` to every formal replica's candidate, full original row, and absence. `source_node_id` selects one existing complete record. Archive-only treatment requires `archive_only: true` and `source_node_id: 0`; it does not recreate a list entry from an isolated replica. Preserving conversations beyond the list cap does not authorize recovering arbitrary single-copy conversations. Reviewed CMD archive-only policy can likewise bind exact divergent CMD conversation groups. Changed, unused, or already-agreeing groups fail; all originals remain archived. An archive-only group with a pending recovery intent also fails rather than recreating that conversation.
Changed rows, mismatched evidence, and unused decisions fail. Formal replicas without an exact reviewed exception must still agree, every original row remains archived, and import and independent verification rebuild the selection. Archive the complete differences and decide the business rule before generating bindings. See the [operator reference](https://github.com/WuKongIM/WuKongIM/blob/main/cmd/wkcli/internal/migrate/README.md) for fields.
Optional: duplicate chains, independent unread counters, and absent conversations
Cross-rule duplicate winners fail by default. After review, `messages.resolve_duplicate_chains: true` follows strictly increasing original sequences within one channel to a unique surviving terminal. It never resurrects older messages. Multiple terminals, changed identities, excluded terminals, and cross-channel MessageID conflicts still fail. The report binds chain-root and terminal counts and a digest; `sequence_mapping.duplicate_chain_proof` references a JSONL sidecar containing direct winners and terminal proofs. The source archive retains complete original messages.
With explicit approval, `metadata.derive_unread_from_boundaries: true` archives independent v2 unread counters and uses native badge calculations over retained messages and mapped read/delete positions, join/retention floors, and the user's latest send. `archived_unread` binds original conversation rows, nonzero-counter counts, and their digest. Displayed counts may differ; this is an approved projection, not proof of counter equivalence. Original read/delete positions are neither cleared nor advanced. Recovery barriers and other `SyncOnce` internal records consume sequences but do not count toward unread badges. A sequence difference is not a message count; set-unread and legacy unread pulls must retain actual ordinary-message boundaries. Include send/receive badge checks after failover. If no released package includes the required fixes, use the [source-build steps in the tools overview](/en/server/tools). The reproducible source baseline for the added migration capabilities in this guide is [`48e89136e`](https://github.com/WuKongIM/WuKongIM/commit/48e89136e5d5f23705ec678bd9ebd5c63c3837cd). Check out that full commit before building both `wkcli` and `wukongim` from the same working directory; inject matching version, commit, and `source` build identity, then compare `version --output json`. This is a source-build baseline, not a published package version. Rehearse with your own complete cold backups before cutover.
This example uses a Linux build machine and target nodes with the same operating system and architecture. Install Go first and use a new directory name. For other platforms, build matching binaries while retaining the same source commit and build identity.
```sh
git clone https://github.com/WuKongIM/WuKongIM.git WuKongIM-migration
cd WuKongIM-migration
git checkout --detach 48e89136e5d5f23705ec678bd9ebd5c63c3837cd
MIGRATION_COMMIT=$(git rev-parse HEAD)
MIGRATION_VERSION=migration-48e89136e
GOTOOLCHAIN=go1.25.11 GOWORK=off CGO_ENABLED=0 go build -trimpath -ldflags="-X main.buildVersion=$MIGRATION_VERSION -X main.buildCommit=$MIGRATION_COMMIT -X main.buildSource=source" -o ./bin/wkcli ./cmd/wkcli
GOTOOLCHAIN=go1.25.11 GOWORK=off CGO_ENABLED=0 go build -trimpath -ldflags="-X main.buildVersion=$MIGRATION_VERSION -X main.buildCommit=$MIGRATION_COMMIT -X main.buildSource=source" -o ./bin/wukongim ./cmd/wukongim
./bin/wkcli version --output json
./bin/wukongim version --output json
sha256sum ./bin/wkcli ./bin/wukongim
export PATH="$(pwd)/bin:$PATH"
```
All target nodes must use matching versions and follow that version’s data-format rollback restrictions.
For a valid member with history but no authoritative conversation, bind `metadata.missing_conversations` to `capture_digest`, `uid_sha256`, `channel_sha256` (SHA256 of `IdentityKey(channelID, channelType)`), and the transformed `retained_tail`. Set `visibility: "hidden_until_new_message"` to preserve membership and history access while suppressing the conversation list entry until a newer ordinary message exists; recovery barriers do not reveal it, while explicit activation does. This marker changes neither history access nor read/delete floors. An absent original read position is not invented; unread after a new message follows native rules. An archived replica conversation additionally requires its exact `conversation_replicas` decision. Pending intents, unapproved existing conversations, changed tails, or unused decisions fail. `hidden_memberships` reports the applied count.
Omitting `visibility` retains the previous explicitly approved fully-read recovery behavior. These are different choices. Hidden membership rows and RPC responses use extended encodings: deploy matching server and CLI versions on every target node. Do not mix older binaries; rollback restores the complete pre-migration data generation and its binaries rather than opening new target directories with old programs.
Optional: quarantine individually confirmed malformed source rows
Tools supporting this feature accept a top-level `quarantine` array. Each entry binds `node_id`, `shard`, the Base64-encoded original aggregated primary `key`, the complete original row JSON `sha256`, and the business-approved `reason`. Bind every physical replica separately. Missing rows, changed hashes, duplicate entries, or a mismatched reason stop migration; table filters and wildcards cannot bypass validation. Check the tool version first: older tools reject this field.
Only three reasons are supported: invalid message channel identity (`invalid_message_channel`), an allowlist member whose channel cannot be resolved from the complete sources (`unresolved_allowlist_channel`), and a CMD-suffixed channel with an inconsistent conversation type (`inconsistent_cmd_conversation`). Message quarantine also requires the approved sequence transformation policy; CMD conversation quarantine additionally requires `exclude_cmd`. This does not authorize clearing other permissions, unread state, or expiration fields.
The original primaries, indexes pointing directly to them, and tails with no surviving source messages remain in the archive. Quarantined positions inside valid channels still receive sequence mappings: `omitted` is `quarantined_invalid_message_channel`, `target_seq` is zero, and `boundary_seq` preserves the preceding surviving-message boundary. Historical gaps without corresponding original-row evidence still stop migration. Archive import revalidates reasons, original hashes, and index dependencies before independently rebuilding mappings.
## 3. Migrate and independently verify [#3-migrate-and-independently-verify]
Keep targets stopped. Run `prepare` → `export` → `import` → `verify`; each exits `0`. Check `prepared`, `COMPLETE`, `imported` and `offline_verified` respectively.
**`offline_verified` means offline success only; `cutover_ready: false` is expected.** Export uses the prepare seal; import/verify use separate workspaces and rebuild complete checks, never skip them. Retain all reports, digests and any enabled sequence mappings.
Expand and run: four-stage commands and report requirements
Run this command block on the migration machine. The four stages check and prepare data, seal the source archive, generate v3 directories, and independently verify them. **A failed command stops the block: read its report before continuing. Keep all targets stopped.**
```sh
(
set -e
umask 077
wkcli migrate prepare --plan /srv/wkmigrate/plan.json \
--workspace /srv/wkmigrate/work/prepare > /srv/wkmigrate/reports/prepare.json 2> /srv/wkmigrate/reports/prepare.stderr
wkcli migrate export --plan /srv/wkmigrate/plan.json \
--workspace /srv/wkmigrate/work/prepare --archive /srv/wkmigrate/source-archive \
> /srv/wkmigrate/reports/export.json 2> /srv/wkmigrate/reports/export.stderr
wkcli migrate import --plan /srv/wkmigrate/plan.json \
--workspace /srv/wkmigrate/work/import --archive /srv/wkmigrate/source-archive \
> /srv/wkmigrate/reports/import.json 2> /srv/wkmigrate/reports/import.stderr
wkcli migrate verify --plan /srv/wkmigrate/plan.json \
--workspace /srv/wkmigrate/work/verify --archive /srv/wkmigrate/source-archive \
> /srv/wkmigrate/reports/verify.json 2> /srv/wkmigrate/reports/verify.stderr
cat /srv/wkmigrate/reports/verify.json
)
```
Each command must exit with code 0. The preparation report must say `prepared`, the import report `imported`, and the final report **`offline_verified`**. A final `cutover_ready: false` is expected: runtime and client acceptance still belong to the next step.
What verification checks
`prepare` checks original formats, indexes, and shards. It uses durable configuration, Slot logs, and applied positions to establish authoritative sources and compare official replicas. It does not choose the longest follower when replicas conflict, modify v2, or create the target cluster.
Successful `prepare` records an `archive_seal` binding its report and all original, catalog, selected, and plugin-artifact rows. `export` rechecks unchanged source and plugin files and validates every exported row against that seal, without repeating catalog joins, replica selection, and conversion. It publishes `COMPLETE` only after the digest matches. Older workspaces without a seal require preparation in a fresh directory with matching tools. `import` and `verify` use separate workspaces and independently rebuild all checks from the complete original archive; the export seal never replaces independent verification.
`verify` independently derives expected values from original archived records. It compares credentials, permissions, conversations, messages, native indexes, commit boundaries, and bootstrap artifacts in every target replica, field by field, not just counts. Confirm that `selection_digest` matches preparation, node counts match the plan, and `verified_message_replicas` equals retained business messages × message replica count. Keep every report, digest, import/exclusion count, and any `sequence_mapping` file and checksums.
**Preserve the attempt first.** Read reports and `.stderr`, confirm the original process has exited, and never start a second concurrent migration or overwrite logs.
* `prepare`: unchanged sources, plan and tools allow workspace reuse under the full retry rules, with separate logs; data is rechecked.
* `import` / `verify`: targets must never have started; import retries also require the unchanged plan and archive.
* Changed sources, target, policy or tool version: retain the old attempt and restart with new workspaces, archive and targets.
Failures and interruptions: diagnosis and retries
Preparation reports stage starts, completion or failure, and elapsed time for source capture, plugins, quarantine, the identity catalog, index validation, authoritative replica comparison, conversion, and the completion record. Use these durations to locate bottlenecks. A completed cold backup does not mean validation or import has finished, and a stage-start log is not success. Index validation uses a sorted merge, but total migration time still depends on every stage and the actual data.
Migration time also depends on index counts, validation lookups, and workspace reads and writes; backup size alone is insufficient to estimate it. If stage logs remain unchanged, first check that the original process is still running, then inspect CPU, logical reads, and physical disk reads. High CPU and repeated logical reads with little physical disk traffic can indicate workspace read-cache starvation. Confirm that the tool includes the migration cache fix; do not skip index or data validation. Validate performance repairs in an isolated rehearsal, and preserve the old attempt when changing tools as described below.
Before freezing writes, measure complete stage durations in an isolated rehearsal and allow sufficient task, session, and service-manager timeouts for the final run. Backup time or one stage's time does not bound the entire migration; `import` and `verify` independently rebuild validation too. For `context canceled`, also check whether an outer task timed out or sent a signal before treating it as a data conflict. Once the original process has exited, unchanged sources, plan, and tools permit rerunning `prepare` with the same workspace and separate logs. It rechecks completed data rather than continuing directly from the last logged stage.
A separate `diagnose` command is not required for the first migration. To inventory issues beforehand or investigate a blocker, use a separate diagnostic directory:
```sh
wkcli migrate diagnose --plan /srv/wkmigrate/plan.json \
--workspace /srv/wkmigrate/work/diagnose > /srv/wkmigrate/reports/diagnose.json
```
Exit code 1 may indicate business blockers or an incomplete scan; read the report and full findings. Successful diagnosis does not establish authoritative replicas, and its workspace cannot be used for `prepare`. Nonempty notification queues block completion; empty queues do not prove external receipt of every old notification.
Resolve authority, duplicate credential, missing conversation, and plugin blockers individually. See `authority`, `dedupe-plan`, and capture-bound decisions in the [engineering runbook](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/runbooks/v2-to-v3-migration.md). Do not delete source rows, clear fields, or rewrite proofs to bypass checks.
Keep the plan, archive, targets, and original logs. If targets have never started and the plan and archive are unchanged, rerun the same `import` command. Completed portions are fingerprint-checked; data from a different migration generation is not overwritten. Verification of unstarted targets can be rerun in full. Save each attempt's logs separately. Directories without migration identity are rejected; do not remove identity or completion markers to force reuse.
To rehearse the full offline flow in isolated Docker containers, use the repository's [rehearsal scripts and examples](https://github.com/WuKongIM/WuKongIM/tree/main/scripts/migration). They provide read-only sources, separate workspaces, disk/time guards, and `--dry-run`. The wrapper does not automatically resume an existing output directory; inspect the attempt under the rules above before recovery.
Do not rerun the entire block and overwrite old reports. When changing sources, targets, policies, or tool versions, preserve the old attempt and restart with new workspaces, a new archive, and entirely new targets.
## 4. Deploy, test, and cut over [#4-deploy-test-and-cut-over]
Save full, verified targets and reports before first startup. Distribute complete directories while stopped; check inventories, checksums and permissions. Match binaries, configuration and plan. Test original UID/Token with the same device\_flag (reject bad tokens; never overwrite credentials), history, conversations/unread, new messages, restarts, plugins and integrations in isolation. File checks and `/readyz` do not replace business acceptance.
Use isolated copies for write tests; production targets receive no business writes before cutover. Started databases cannot be verified as the initial import state or overwritten by import. If renumbering, handle drafts and unsent messages first and retain login credentials; the tool never clears client caches automatically.
**Cutover gate:** never let old and new clusters accept writes for the same business simultaneously. Rehearsed v2 rollback is possible only before new v3 production writes; afterward, no direct switch back or reverse incremental v3 → v2 migration exists. In three-node/three-replica tests, quorum writes on an existing channel do not imply full readiness; restore readiness on every node before routing production traffic.
Full distribution, deployment, acceptance and rollback order
1. **Preserve complete target snapshots and reports first.** Copy each full target directory to the corresponding v3 server at the **same absolute path**; compare file inventories, checksums, and permissions. Keep every target stopped. If the migration machine is also a target server, leave that node's directory in place and distribute only the others. Do not repeat `import`.
2. Configure `wukongim.toml` according to the plan. Start matching v3 binaries in isolation and test original-token login, history and conversation read state, new sequences and unread counts, restart recovery, plugins, and external integrations. Run write tests on isolated rehearsal copies. Configuration and acceptance details are below.
3. If sequences were renumbered, clear old client message caches and sync cursors, **preserve login credentials**, and resync from v3. Handle unsent messages and drafts separately first. Switch routing and gradually restore traffic only after all acceptance checks pass. Never allow both generations to accept business writes simultaneously.
**Started databases cannot be verified as initial import state or overwritten by another import.** Returning to original v2 is possible only before v3 accepts new production writes. Switching back afterward would lose new data; this tool has no reverse incremental v3 → v2 migration.
Multiple servers: distribute one target node
Run on the migration machine, replacing the target hostname and node ID. Repeat for the other targets. The example checks that the remote target path does not exist before copying, to avoid overwriting existing data. Keep the target stopped.
```sh
ssh root@v3-a 'test ! -e /srv/wkmigrate/targets/node101 && test ! -L /srv/wkmigrate/targets/node101 && mkdir -p /srv/wkmigrate/targets' && \
rsync -a --numeric-ids --partial /srv/wkmigrate/targets/node101 root@v3-a:/srv/wkmigrate/targets/
rsync -anc --numeric-ids --delete --itemize-changes /srv/wkmigrate/targets/node101/ root@v3-a:/srv/wkmigrate/targets/node101/
```
After the copy succeeds, run verification. `-n` is a dry run and deletes nothing. Require exit code 0 and no differences, save the results, then grant access to the actual service account. Copy the entire directory, including Controller snapshots, migration markers, `DATA-FORMAT.json` when present, and hidden files. Inspect format and creator with [wkcli db info](/en/server/tools/wkdb#inspect-the-data-version); older outputs without a marker remain unregistered. File comparison does not replace business verification in step 3.
If copying is interrupted, confirm that the receiving directory belongs only to this attempt, has never started, and the migration output is unchanged. Then resume that copy and verify it in full; never delete or overwrite another migration's data.
Startup configuration and acceptance checklist
Prepare each node's `wukongim.toml`. Match `node.id`, `node.data_dir`, `cluster.id`, `cluster.nodes`, `cluster.initial_slot_count`, `cluster.hash_slot_count`, `cluster.slot_replica_n`, and `cluster.channel_replica_n` to the plan. Separately check TLS, listening/advertised addresses, gateway authentication, Webhook endpoints and network protections, plugins, and business backend configuration. Database import does not transfer or validate those settings. Native Webhooks do not provide signatures; if your business uses an external signing proxy, migrate and validate that configuration separately. See [Cluster Configuration](/en/server/configuration/cluster) and [Security Configuration](/en/server/configuration/security).
For a three-node Docker Compose cluster, bind a separate target directory to each service and use the matching v3 image. On a bridge network, both the plan's `target.nodes[].addr` and the configuration's `cluster.nodes` must use service names or network aliases reachable between containers, such as `wk-node1:7000`; the listener may bind to `0.0.0.0:7000`. Do not use host loopback addresses or published host ports as inter-container RPC addresses. Keep the container's `node.data_dir` equal to the verified plan path, and ensure the runtime user can read and write the bind mount. For 12 logical Slot Raft Groups, 256 physical hash slots, and three replicas, set `initial_slot_count=12`, `hash_slot_count=256`, `slot_replica_n=3`, and `channel_replica_n=3`. Finalize addresses before `prepare`; a changed plan requires a fresh workspace and migration artifacts, never an edited migration identity inside an existing target. Run `docker compose config --quiet` and each node's `wukongim config validate`, then complete offline verification before starting containers.
For persistent CMD recovery, adapt the sending service before cutover: a normal group subscription or a successful `sync_once=1` SEND does not create a CMD discovery binding. Call `POST /message/cmd/bind` for each authorized recipient and stable source Channel **before** the first command that must survive disconnection; maintain those bindings with membership changes, and call `/message/cmd/unbind` when access ends. Bind takes effect after the current CMD tail and does not recover commands sent before it. Request-scoped `subscribers` alone are not an offline recovery directory. On a target version supporting batch bindings, first bind `{ "subscribers": [...] }` in exactly the same order as SEND. For a stable source Channel, use `{ "uids": [...], "channel_id": "...", "channel_type": 2 }` in batches of at most 1000 entries and 256 KiB per request. Do not split a temporary recipient scope and then send to the original scope: different scopes derive different CMD Channels. Cross-Slot binding failures can be partial; retry and confirm the complete binding before sending. If the old service relies on automatic offline fanout, importing data alone does not provide that compatibility. Test an actually disconnected recipient, `/message/sync`, processing and `/message/syncack` across nodes, including removal; online CMD delivery is a separate check. The current `/message/syncack` uses process-local sync records: pin each sync/ack pair to the same server process instead of independently load-balancing them. After acknowledgement, read through other nodes to verify the durable position. Also disband one bound test source Channel and confirm global CMD sync still returns commands from other live sources. Confirmed terminal sources must be skipped without allowing one stale binding to block all synchronization; timeouts and unavailable reads must remain errors. See [recoverable commands](/en/guide/tutorials/ai-and-iot).
Start all targets in isolation using the matching v3 server, without production traffic. At minimum, check:
* `/readyz` succeeds on every node, and Controller, logical Slot Raft Groups, and channel replicas are healthy.
* Original tokens with the same `device_flag` authenticate; incorrect tokens are rejected. Do not reset tokens to “validate” old credentials.
* First, last, and cross-page history matches payloads, IDs, sequences, ClientMsgNo, and RedDot. Permissions, conversation lists, and read state match the plan.
* New message sequences strictly exceed the current channel tail, retrying the same idempotency key does not duplicate messages, and new unread counts are correct. For person sends, use the recipient UID, not the peer UID from history queried as the recipient.
* Verify group add/remove system messages and CMD delivery, excluding removed members from later messages. Test exact `POST /messages` lookups and application recall notifications/display separately from full-text search. The lookup accepts `message_ids`, `message_seqs`, and `client_msg_nos` with `login_uid`, `channel_id`, and `channel_type`; at most 128 selectors, 1024 results, 16 MiB of payload and 4096 inspected client-index entries per key are allowed. Exhaustion fails explicitly. All nodes must use matching versions supporting this query.
* History, new messages, and unread state survive full-cluster restarts. For multi-node clusters, test single-node failure recovery under the agreed plan.
For three nodes with three replicas, distinguish existing channels from new channel placement. After stopping one node, existing channels may still commit through quorum while `/readyz` returns 503 because there are too few eligible nodes for new channel placement. Create a test channel while all nodes are ready, then stop each node in turn and verify that channel's history, new writes, and recovery. Preserve the readiness failure reason; passing existing-channel checks does not prove full readiness. Cut production traffic over only after all target nodes recover and pass readiness checks. See [Multi-node Deployment](/en/server/deployment/multi-node).
* Retained event projections, plugins, Webhooks, push notifications, and actual business clients pass their own acceptance tests.
Run write tests against isolated rehearsal copies. API checks do not replace SDK login, actual UI, and external integration acceptance. The [Chat Demo](/en/guide/quick-start/chat-demo) supports testing with existing tokens; do not create or overwrite original credentials.
Client mapping and rollback details
Renumbering invalidates old message caches and sequence cursors. For a cache-reset cutover, clear client history copies and derived sync cursors by migration generation, **preserve login credentials**, then sync conversations and history from v3. Handle unsent messages and drafts separately before clearing caches to avoid deleting or resending pending work. `wkcli` migrates WuKongIM data only and does not clear client caches.
If clients use a mapping instead, they must handle the complete `sequence_mapping`. Excluded rows have `target_seq` 0; use `boundary_seq` for old cursors rather than treating excluded rows as existing messages. Rebuild the mapping from the archive with:
```sh
wkcli migrate export-map --plan /srv/wkmigrate/plan.json \
--workspace /srv/wkmigrate/work/map \
--archive /srv/wkmigrate/source-archive
```
After all reports, runtime tests, and client acceptance pass, operators can switch routing and gradually restore traffic. Never allow both generations to accept writes for the same business simultaneously.
| Stage | Rollback method |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Before v3 accepts new production writes | Close new-cluster ingress, restore original v2 cold backups and routing using the rehearsed procedure, and restore the corresponding client migration generation |
| After v3 accepts new production writes | Do not switch directly to old v2: new data would be lost. Preserve the new databases and use a validated v3 repair or backup recovery procedure |
This tool has no reverse incremental v3 → v2 migration. Before cutover, rehearse restoration of the original version, data, and routing using backup copies, and record the recovery duration. Never use the preserved original backup for write tests.
# v2 → v3 Offline Migration (/en/server/operations/v2-to-v3-migration)
Move into a fresh v3 single-node cluster in five steps. For multi-node deployments, plugins or compatibility issues, start with the [migration reference](/en/server/operations/v2-to-v3-migration-reference).
## The environment for this example [#the-environment-for-this-example]
The source is original `v2.2.5-20260422` (node `1001`, DB shards `8`); the migration machine also runs target v3 (node `1`). Substitute actual source values. This example excludes plugins and external integrations.
* **Stop writes and all source nodes; use a fresh empty target cluster.** v3 cannot open v2 directories; online incremental migration is unsupported.
* Use matching `wkcli` and `wukongim` binaries and verify delivery digests. Assess other versions, custom builds and required capabilities first.
* Rehearse in isolation to measure full duration and peak space for complete backups, workspaces, the archive, target replicas and pre-start snapshots.
Full environment, example values and prerequisites
This walkthrough migrates users, credentials, conversations, and messages from original v2 into a fresh v3 single-node cluster. Follow the steps in order: **back up → write a plan → migrate → start and test → cut over**.
For multi-node clusters, use the [three-node plan in the migration reference](/en/server/operations/v2-to-v3-migration-reference#2-fill-in-one-planjson). Include every source node and run the migration centrally on one machine; do not take just one node's data from a multi-node cluster.
This example uses a separate Linux migration machine that also runs v3 after migration. It excludes plugins and external integrations. Rehearse in isolation first.
| Item | Example value |
| ------------------------------------ | ----------------------------------------------------------------- |
| Original v2 | Original `v2.2.5-20260422`, single-node cluster, node ID `1001` |
| Original business DB shard count | `8`; replace with the actual value from the old configuration |
| Stopped v2 server | SSH hostname `v2-a`, complete data directory `/opt/wukongim/data` |
| Cold backup on the migration machine | `/srv/v2-snapshots/node1001/data` |
| Migration working directory | `/srv/wkmigrate`; must be a new directory |
| Target v3 | Node ID `1`, RPC address `127.0.0.1:7001` |
| Target data directory | `/srv/wkmigrate/targets/node1`; created by the migration tool |
Replace the **v2 hostname, original data path, node ID, and shard count** with your actual values. You can keep the remaining example paths. Run the commands in a Bash terminal on the migration machine with permission to write under `/srv`.
Confirm three things before starting:
* **All sources are stopped and the target is a fresh empty cluster.** v3 cannot open v2 data directories directly, and online incremental migration is not supported.
* Follow [wkcli installation](/en/server/tools/wkcli) to obtain matching `wkcli` and `wukongim` binaries and verify the delivered checksums. The current documentation version is `v3.0.0-beta.24`. For other v2 versions, custom builds, plugins, or external integrations, read the [compatibility requirements](/en/server/operations/v2-to-v3-migration-reference#1-collect-backups-and-install-the-tools) first.
* Allow disk space for complete cold backups, workspaces, the archive, target data, and pre-start snapshots. Rehearse to measure peak space and total duration before scheduling the production maintenance window.
## 1. Take a complete cold backup [#1-take-a-complete-cold-backup]
Stop write ingress; drain log application, topology changes and notification queues, then stop v2 gracefully and disable automatic restarts. Copy the full directory including hidden files; retain old binaries, configuration and environment variables. Never copy a running database.
**Pass:** `rsync` verification exits `0` with no differences; `-n` only checks and deletes nothing. If v2 writes resume after rehearsal, production migration needs fresh backups and new working directories.
Backup, copy verification and rehearsal commands
On the original deployment, stop business write ingress, wait for log application, topology changes, and notification queues to drain, then stop v2 gracefully and disable automatic restarts. Preserve the old binaries, configuration, and environment variables. **The directory copied below must no longer be changing. Do not copy a running database.**
On the migration machine, run:
```bash
umask 077
mkdir -p /srv/v2-snapshots/node1001/data
rsync -a --numeric-ids --partial \
root@v2-a:/opt/wukongim/data/ /srv/v2-snapshots/node1001/data/
```
After copying succeeds, verify the copy:
```bash
rsync -anc --numeric-ids --delete --itemize-changes \
root@v2-a:/opt/wukongim/data/ /srv/v2-snapshots/node1001/data/
```
**Pass condition: exit code 0 and no file differences.** Here, `-n` makes verification a dry run that deletes nothing. Keep the trailing `/`; include the whole directory and hidden files. If you already have a complete cold backup, copy from that backup without restarting v2.
You can restore original v2 service after taking the rehearsal backup. For production cutover, stop writes and all sources again, take a fresh backup, and repeat the remaining steps in new working directories. Writes made during rehearsal do not automatically enter the earlier migration output.
## 2. Save the migration plan [#2-save-the-migration-plan]
Both binaries must share `version`, `commit` and `build_source`. Plan paths are absolute paths on the migration machine; sources, workspaces, archive and targets must not contain one another.
**Pass:** `plan.json` is saved in a new working directory and `targets/node1` does not yet exist. The example uses **12 logical Slot Raft Groups, 256 physical hash slots and one replica**, matching startup configuration. `source_commit` selects reader rules; it cannot declare arbitrary old data compatible. Deduplication, omissions and renumbering stay off by default.
Tool checks, directory commands and single-node plan.json
First check the tools:
```bash
wkcli version --output json
wukongim version --output json
```
Both binaries must have identical `version`, `commit`, and `build_source` values. Then create the working directory. If `/srv/wkmigrate` already exists, choose a new path and update the following paths consistently; do not overwrite an earlier attempt.
```bash
mkdir /srv/wkmigrate && mkdir -p /srv/wkmigrate/{reports,work,targets}
```
Save this as `/srv/wkmigrate/plan.json`, updating the source node, shard count, and creation timestamp for this attempt:
```json
{
"version": 1,
"source_commit": "a888f89533d0e7d1b2030e06504ca97f1ad891d4",
"sources": [
{"node_id": 1001, "data_dir": "/srv/v2-snapshots/node1001/data", "shard_count": 8}
],
"target": {
"cluster_id": "migration-v3-new",
"created_at": "2026-09-12T00:00:00Z",
"slot_count": 12,
"hash_slot_count": 256,
"replicas": 1,
"channel_replicas": 1,
"nodes": [
{"node_id": 1, "addr": "127.0.0.1:7001", "data_dir": "/srv/wkmigrate/targets/node1"}
]
}
}
```
`source_commit` selects the supported v2 reader schema. Keep this value; it is not a guessed image version. The target uses 12 logical Slot Raft Groups, 256 physical hash slots, and one replica. All paths are absolute paths on the migration machine and must not contain one another. **Do not create `targets/node1` beforehand.**
This plan does not enable deduplication, CMD/stream exclusion, or sequence renumbering. If compatibility checks block migration, use the troubleshooting section below rather than applying data-discard policies just to pass validation.
## 3. Run migration and reach offline\_verified [#3-run-migration-and-reach-offline_verified]
Keep v3 stopped. Run the four stages in order on one migration machine; stop on failure and read that stage’s report.
**Pass:** every stage exits `0`, ending in `offline_verified`. **`cutover_ready: false` is expected:** runtime and business acceptance still follow. Retain cold backups, archive, plan and all reports.
Expand and run: all four stages and report requirements
Keep v3 stopped. Run this command block once on the migration machine. A failed stage stops the block immediately; reports are saved under `reports`.
```bash
(
set -e
umask 077
wkcli migrate prepare --plan /srv/wkmigrate/plan.json \
--workspace /srv/wkmigrate/work/prepare \
> /srv/wkmigrate/reports/prepare.json 2> /srv/wkmigrate/reports/prepare.stderr
wkcli migrate export --plan /srv/wkmigrate/plan.json \
--workspace /srv/wkmigrate/work/prepare --archive /srv/wkmigrate/source-archive \
> /srv/wkmigrate/reports/export.json 2> /srv/wkmigrate/reports/export.stderr
wkcli migrate import --plan /srv/wkmigrate/plan.json \
--workspace /srv/wkmigrate/work/import --archive /srv/wkmigrate/source-archive \
> /srv/wkmigrate/reports/import.json 2> /srv/wkmigrate/reports/import.stderr
wkcli migrate verify --plan /srv/wkmigrate/plan.json \
--workspace /srv/wkmigrate/work/verify --archive /srv/wkmigrate/source-archive \
> /srv/wkmigrate/reports/verify.json 2> /srv/wkmigrate/reports/verify.stderr
cat /srv/wkmigrate/reports/verify.json
)
```
| Stage | Purpose | Pass condition |
| --------- | -------------------------------------------------------------- | ------------------------------------------- |
| `prepare` | Check cold backups and prepare conversion | Exit code 0, report says `prepared` |
| `export` | Seal original data into an archive | Exit code 0, archive contains `COMPLETE` |
| `import` | Generate v3 data directories | Exit code 0, report says `imported` |
| `verify` | Independently compare target data against the original archive | Exit code 0, report says `offline_verified` |
**`cutover_ready: false` is expected.** Offline data verification has passed; runtime and business acceptance still follow. Preserve the cold backups, archive, plan, and all reports.
## 4. Start v3 and test real business behavior [#4-start-v3-and-test-real-business-behavior]
First startup changes the databases. **Save a full, verified target snapshot before any first start.** Match configuration to the plan, retain generated secrets and check `WK_` overrides. Keep `gateway.token_auth_on=true`; adapt the original v2 person-whitelist setting separately.
**Pass:** `/readyz` returns `200` and `ready: true`, then test original UID/Token with the same device\_flag (reject bad tokens; never overwrite credentials), early/recent/paged history and read/unread state, new sequences and retry deduplication, and persistence after restart. Plugins and external integrations need separate acceptance.
Run write tests on isolated rehearsal copies. Production targets receive no business writes before cutover. **Started directories cannot be verified as the initial import state or overwritten by another `import`.**
Snapshot, configuration, startup and full business acceptance
First startup changes the databases. Save a snapshot of the verified targets before they have ever started. In this example, check that the snapshot path does not exist, then run:
```bash
test ! -e /srv/wkmigrate/targets-before-start && \
cp -a /srv/wkmigrate/targets /srv/wkmigrate/targets-before-start && \
wukongim config init --config /srv/wkmigrate/wukongim.toml
```
Confirm the snapshot copy succeeded, and save the administrator password displayed during initialization. Edit the generated `wukongim.toml`, changing these two values while preserving all other settings and generated secrets:
| Configuration location | Change to |
| ------------------------- | -------------------------------- |
| `data_dir` under `[node]` | `"/srv/wkmigrate/targets/node1"` |
| `id` under `[cluster]` | `"migration-v3-new"` |
Check that generated values match the plan: `node.id=1`, `cluster.listen_addr="127.0.0.1:7001"`, `cluster.nodes=[{id=1,addr="127.0.0.1:7001"}]`, `initial_slot_count=12`, `hash_slot_count=256`, `slot_replica_n=1`, and `channel_replica_n=1`. Keep `gateway.token_auth_on=true`. If original v2 used `whitelistOffOfPerson=false`, also set `message.person_whitelist_enabled=true`.
Confirm that no leftover `WK_` environment variables override these values, then validate and start in the foreground:
```bash
wukongim config validate --config /srv/wkmigrate/wukongim.toml && \
wukongim -config /srv/wkmigrate/wukongim.toml
```
Keep that terminal open. In another terminal on the migration machine, check:
```bash
curl --fail http://127.0.0.1:5001/readyz
```
After `/readyz` succeeds, test these four areas with your actual client or the [Chat Demo](/en/guide/quick-start/chat-demo). Default listeners use loopback addresses; for remote access, use SSH forwarding as described in [Linux deployment](/en/server/deployment/linux).
* **Login works:** use the original UID, Token, and matching `device_flag`. Incorrect tokens must be rejected. Do not register again and overwrite original credentials.
* **Old data matches:** check early, recent, and cross-page person/group history, including payloads, message IDs, sequences, conversation lists, and read/unread state.
* **Messaging continues:** send new messages on isolated rehearsal data. Check receipt, unread counts, retry deduplication, and new sequences greater than the channel's current tail.
* **Data survives restart:** stop v3 gracefully, start it again, and recheck history, new messages, and unread state.
Write acceptance in this example is for rehearsal. Production migration also requires acceptance, but run write tests on isolated copies and keep production targets free of business writes until cutover. **Started directories cannot be verified as the initial import state or overwritten by another `import`.**
## 5. Cut over production [#5-cut-over-production]
Keep v2 writes stopped. Run v3 under a service manager with the verified configuration and same data directory. Switch backend/client routes only after all acceptance passes. If renumbering, handle drafts and unsent messages first, retain login credentials, then handle old caches and sync cursors; the tool never clears client caches automatically.
**Pass:** only one cluster accepts writes for the same business.
* Before new v3 production writes: close new ingress and restore original v2 data, binaries and routes using the rehearsed procedure.
* After new v3 production writes: never switch directly back to old v2 and lose new data. There is no reverse incremental v3 → v2 migration.
Full production cutover and rollback boundaries
After rehearsal, repeat migration during the production maintenance window using fresh complete cold backups and new working directories. Complete offline verification and runtime/client acceptance. Keep original v2 writes stopped. Run v3 under a service manager following [Linux deployment](/en/server/deployment/linux), confirming it uses the verified configuration and the same data directory. Then point business backend and client connection/routing endpoints to v3 and gradually restore traffic.
If you enabled sequence renumbering from the reference, also handle client history caches and sync cursors during cutover. Preserve login credentials and handle unsent messages and drafts first. The tool does not clear client caches automatically.
**v2 and v3 must never accept writes for the same business simultaneously.** Before v3 accepts new production writes, you can close v3 ingress and restore original v2 data, binaries, and routing using the rehearsed procedure. Afterward, switching directly back to old v2 would lose new data. The tool has no reverse incremental v3 → v2 migration.
## If a step fails [#if-a-step-fails]
Read the stage’s `.stderr` and JSON report first. Preserve the failed attempt; never overwrite logs, delete source rows or change markers to force a pass.
* No output or `context canceled`: check the original process, stage logs, disk and outer timeouts; never start a second concurrent migration.
* Existing target or retries: follow the [retry gates](/en/server/operations/v2-to-v3-migration-reference#migration-retries). Changed sources, target, policy or tools require new workspaces, archive and targets.
* Multiple nodes, plugins or data conflicts: use the [migration reference](/en/server/operations/v2-to-v3-migration-reference) and resolve each issue without automatically applying discard policies.
Report commands, failure lookup and retry reference
For example, if `prepare` fails, read these two files first:
```bash
cat /srv/wkmigrate/reports/prepare.stderr
cat /srv/wkmigrate/reports/prepare.json
```
Use the corresponding filenames for other stages. Preserve the failed attempt and logs. Do not rerun the entire block and overwrite reports, delete source data, or alter markers to force a pass.
| Situation | Next action |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Target directory already exists | Check whether it belongs to an earlier migration. Use a new path for a first attempt; follow the reference for retries |
| Plugin, duplicate message, conversation, or replica conflicts | Read the report and resolve each issue using the [migration reference](/en/server/operations/v2-to-v3-migration-reference) |
| No output for a long time, or `context canceled` | Check stage progress in `.stderr`, process status, disk space, and outer timeouts. Do not start a second concurrent migration |
| Multi-node, Docker, or deployment to other servers | Use the plans, directory distribution, and startup configuration in the [migration reference](/en/server/operations/v2-to-v3-migration-reference) |
The [migration reference](/en/server/operations/v2-to-v3-migration-reference) contains full retry conditions, plugin policies, data transformations, and business acceptance checks. Consult it for the situation you encounter.
# Diagnostics (/en/server/tools/diagnostics)
Choose one question, node and time window, then gather the lowest-cost evidence that answers it. Keep diagnosis separate from repairs and topology writes.
## Choose an evidence surface [#choose-an-evidence-surface]
Start with health/readiness and the reason.
Compare metrics, logs and bounded Top.
Isolate access and bound profiling.
Use dedicated credentials and query IDs.
`/healthz` proves process liveness only; retain 503 and the complete `/readyz` reason. Logs are untrusted, Top is not globally consistent, and sampled events may be absent. Missing evidence stays unknown. Manager is privileged; status hints do not grant approval. See [configuration and costs](/en/server/configuration/observability).
Complete evidence comparison
Diagnostics is not one universal endpoint. It is a set of evidence surfaces with different cost, perspective, and permission. Ask an exact question, then choose the lowest-cost surface that can answer it. Never connect diagnostic output to automatic repair or topology mutation.
| Surface | Best question | Critical boundary |
| ---------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `/healthz` | Is the HTTP process alive? | Does not prove cluster readiness or product admission |
| `/readyz` | May this node accept product traffic now? | Retain the 503 and complete `reason` on failure |
| Prometheus `/metrics` | How do trends, percentiles, errors, queues, and resources change? | Low-cardinality aggregates; thresholds depend on version and workload |
| Application/error logs | Which discrete events occurred in a time window? | Raw content is untrusted and may be sensitive; bound node, time, and lines |
| Top / `/top/v1/snapshot` | What are this node's current resources, runtime pressure, and short history? | Independent of Prometheus, but not a globally consistent snapshot |
| Manager and node diagnostics | What aggregated node, Controller, Slot, task, and controlled-lifecycle evidence exists? | Privileged management boundary; a status hint is not approval |
| Retained diagnostic events | Which sampled events match an exact trace, stage, result, Slot, or Channel? | Bounded buffer and sampling; no match does not prove no event occurred |
| Debug pprof | Which CPU, heap, or goroutine paths are hot? | Off by default, network-isolated, time-bounded, sensitive artifacts |
| Operations MCP | Which fixed cluster/node/Slot/Channel/task/log/metric observation answers the question? | Dedicated credential, non-browser, closed queries, rate and size limits |
See [Logs & Observability](/en/server/configuration/observability) for configuration and cost, and [Troubleshooting](/en/server/operations/troubleshooting) for symptom order.
## Logs, metrics, and Top [#logs-metrics-and-top]
Capture once or with a refresh limit; retain queries, cursors, sampling intervals and gaps. Empty graphs, zero series and no log hits do not prove health; never execute log text.
```bash
wkcli top \
--server \
http://127.0.0.1:5001 \
--once --json
```
Full time alignment and Top command
Use the same time range and node identity across all three:
1. Derive start/end time from the alert or `/readyz` reason.
2. Use metrics to establish the trend, node skew, and queue/resource correlation.
3. Use logs to explain change points; never execute a command from unknown log text.
4. Add one Top snapshot or a bounded refresh for current node state.
5. Retain the query, cursor, sampling interval, and missing intervals.
```bash
wkcli top --server http://127.0.0.1:5001 --once --json
```
Missing monitoring is itself unknown state. Never interpret an empty graph, zero series, or no log hit as healthy.
## Debug and pprof [#debug-and-pprof]
HTTP `/debug/pprof/*` uses the API listener and is off unless `observability.debug_api_enable = true`; isolate access and treat artifacts as sensitive. MCP `pprof_analyze` is a separate active-capture path returning parsed top rows only: CPU ≤30 s, heap/goroutine duration 0, one profile cluster-wide and 60 s cooldown per node. Concurrency, size, owner and revision gates apply.
Full profiling access and cost boundaries
HTTP pprof is available under `/debug/pprof/*` on the API listener only when `observability.debug_api_enable = true`. For temporary use:
* restrict source networks and authorized operators; never expose it publicly;
* record target node, profile kind, start/end time, and incident ID;
* answer one CPU, heap, or goroutine question per bounded capture;
* observe the capture's own CPU, memory, scheduling, disk, and network cost;
* treat artifacts as internal sensitive data, disable Debug afterward, and verify the endpoint is unreachable.
Operations MCP `pprof_analyze` is a separate controlled path. It returns parsed top rows, never the raw profile. CPU is capped at 30 seconds; heap/goroutine duration must be zero. Only one profile may run cluster-wide, each node has a 60-second cooldown after completion, and concurrency, size, owner, and revision fences apply.
## Operations MCP [#operations-mcp]
Manager listeners mount stateless JSON Streamable HTTP `POST /mcp`. Use dedicated `wko_*` bearer credentials, not Manager JWT, and TLS over untrusted networks. The 12-tool registry allows fixed, bounded reads; `pprof_analyze` actively captures. No arbitrary URLs, paths, commands, PromQL, SQL, Controller writes, Resources, Prompts, Sampling, Roots or SSE. Missing evidence returns `unavailable` / `unknown`.
`/mcp` rejects every non-empty `Origin` and grants no CORS access. Requests are capped at 64 KiB and responses at 1 MiB. Authentication, rate, concurrency, owner, or state-change failures return stable errors instead of falling back to anonymous or local execution.
All 12 tools and protocol boundaries
Every configured Manager listener mounts the same `POST /mcp`. It is a stateless JSON Streamable HTTP MCP service using dedicated `wko_*` bearer credentials; a Manager JWT is not an MCP credential. Use TLS across untrusted networks.
The frozen 12-tool registry is:
| Tool | Scope |
| ------------------------------ | --------------------------------------------------------------------------------------- |
| `cluster_health` | Aggregate Controller, node, Slot, workqueue, and metric health without Channel scanning |
| `node_inspect` | One exact node's health, runtime, Controller Raft, workqueues, and bounded diagnostics |
| `slot_inspect` | One exact physical Slot's leader, replicas, progress, and indices |
| `channel_runtime_inspect` | One exact Channel point lookup through its hash Slot, without catalog enumeration |
| `controller_tasks_query` | Bounded active and retained Controller task evidence |
| `metrics_query_range` | A bounded range for a server-owned, low-cardinality query ID |
| `logs_search` / `logs_context` | Bounded search and cursor context over fixed application/error log sources |
| `diagnostics_query` | Retained diagnostics filtered by node, Slot, trace, stage, result, and time |
| `config_read_redacted` | Allowlisted, already-redacted effective configuration for one node |
| `backup_inspect` | Full-backup plan, active task, and bounded immutable archive evidence |
| `pprof_analyze` | Bounded CPU, heap, or goroutine top rows for one node |
All tools are read-only except the active capture performed by `pprof_analyze`. The service accepts no arbitrary URL, filesystem path, command, PromQL, SQL, or general Controller writer. It exposes no Resources, Prompts, Sampling, Roots, SSE, or write tools. Missing evidence returns `unavailable` / `unknown`, never fabricated zero or healthy state.
## Usage boundaries [#usage-boundaries]
Limit credential tools, nodes and lifetime; do not log tokens or execute raw log content. Use bounded server-owned metric query IDs and exact Channel ID/type, never catalog scans. pprof/logs/diagnostics are uncached; short inventories/config may be cached, so retain observation times. Audit summaries omit raw tokens, full arguments/results and log keywords.
Full credential, query, cache and audit limits
* Create a least-scope incident credential with bounded tools, nodes, and lifetime; never log the complete token.
* Log tools return bounded raw lines marked untrusted; never place their content in a shell or prompt-control channel.
* Metrics use server-owned query IDs with bounded range and points; MCP cannot execute arbitrary PromQL.
* Exact Channel lookup requires Channel ID and type and cannot scan the catalog.
* pprof, logs, and diagnostics are not cached; short inventories and redacted config may be briefly cached, so retain observation time.
* MCP audit stores low-cardinality summaries only, not raw tokens, complete arguments, results, or log keywords.
## Remove temporary diagnostics [#remove-temporary-diagnostics]
At incident end, revoke temporary credentials, restore sampling, disable Debug and Benchmark, remove unnecessary local copies, and verify network policy. Retain redacted commands, time ranges, query IDs, cursors, tool result codes, and artifact digests for review and regression testing.
# Tools (/en/server/tools)
One `wkcli`; choose its command family by target. Start with bounded read-only checks, then follow an explicit change procedure.
Every deployment is a cluster, including a single-node cluster. The physical hash-slot count is 256, and the generated default configuration uses 12 logical Slot Raft Groups. A node-local snapshot, database directory, or Top result does not automatically represent the cluster.
## Choose a tool [#choose-a-tool]
Output is evidence. Manager approval, permissions, and cluster safety gates still apply.
Complete tool comparison and boundaries
WuKongIM provides one operator binary, `wkcli`, with command families for each target: `wkcli` uses live cluster APIs, `wkcli db` reads one node's offline data, `wkcli bench` generates real traffic in an isolated target, and diagnostic surfaces answer one bounded question. Tool output is evidence, not permission to bypass Manager approval or cluster safety gates.
| Objective | Use | Read or write | Not for |
| ---------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Inspect live nodes, Top, or dynamic-node state | [wkcli](/en/server/tools/wkcli) | Most inspection is read-only; node-lifecycle subcommands call controlled Manager write APIs | Direct Controller/Slot editing or process management |
| Query, export, or compare node-local storage | [wkcli db](/en/server/tools/wkdb) | Offline and read-mostly; only `import` writes the target store | Online cluster queries, global snapshots, or automatic migration |
| Validate protocol traffic, a capacity hypothesis, or a regression gate | [wkcli bench](/en/server/tools/wkbench) | Generates real connections, channels, users, and messages | Unbounded production load or universal capacity numbers |
| Migrate stopped original v2 backups to a new v3 cluster | [wkcli migrate](/en/server/operations/v2-to-v3-migration) | Reads cold backups and writes new offline target directories | In-place upgrades or online incremental replication |
| Correlate logs, metrics, Top, diagnostics, and pprof | [Diagnostics](/en/server/tools/diagnostics) | Read-only by default; pprof is a bounded active observation | Automatic repair, arbitrary commands, or arbitrary queries |
## Common workflow [#common-workflow]
Bound targets, concurrency, duration, rows, and artifact directories; define stop conditions first. Keep commands, time, exit codes, and redacted output. Manager, Benchmark, Debug, and Operations MCP need separate authorization.
Complete six-step workflow
1. **Confirm identity**: record repository version, binary digest, cluster ID, node ID, and target addresses.
2. **Choose the minimum surface**: begin read-only, low-cost, and short-lived; do not start with profiling or load generation.
3. **Isolate permission**: use least-privilege credentials and controlled networks. Manager, Benchmark, Debug, and Operations MCP have separate trust boundaries.
4. **Set budgets**: bound target, concurrency, duration, returned rows, and artifact directory; write down a stop condition.
5. **Retain context**: keep the command, time range, exit code, and redacted output, not only the final summary.
6. **Remove temporary capability**: disable Debug, Benchmark, temporary credentials, simulators, and extra sampling, then confirm resources return to baseline.
## Obtain wkcli [#obtain-wkcli]
Since `v3.0.0-beta.12`, official archives and Linux native packages include matching `wkcli`. [Install and verify versions](/en/server/tools/wkcli), then choose a command family.
Complete distribution and version history
Since v3.0.0-beta.12, the official archives and Linux native packages include `wkcli` alongside the server. Start with [installation and version checks](/en/server/tools/wkcli). All examples use this one executable; you do not need separate benchmark, database, or migration binaries.
## Build from source [#build-from-source]
Build only a reviewed revision. Record tool and target versions; do not use development-branch tools directly in production or assume unknown versions are compatible. Expand for build commands.
Complete source build and compatibility boundaries
Build the required binary from a reviewed repository revision:
```bash
go build -o ./bin/wkcli ./cmd/wkcli
./bin/wkcli --help
```
For source development, replace `wkcli` in examples with `./bin/wkcli` or `go run ./cmd/wkcli` from the repository root. The latter requires the Go version in `go.mod`.
Record a traceable version for both tool and target server. Do not use a development-branch tool against production or assume offline formats, Manager APIs, or benchmark protocols are compatible across unknown versions.
## Handling failure [#handling-failure]
Stop if identity, authentication, connectivity, or freshness is unclear; do not retry with broader permission. Read authoritative task state after failed writes. Retain conflicting evidence; stop loads, profiles, and scans at their budget.
Complete failure handling and troubleshooting paths
* Stop when authentication, connectivity, state freshness, or target identity cannot be proven; do not keep trying with broader credentials.
* When read-only outputs disagree, retain both and check their time, node, version, and source.
* After a write command fails, read authoritative task state before retrying; the previous request may already have taken effect.
* Stop a load, profile, or scan when its budget is reached even if it has not produced the expected conclusion.
Use [Troubleshooting](/en/server/operations/troubleshooting) to choose the first check from a symptom.
# wkcli bench (/en/server/tools/wkbench)
Start with the smoke workload, then search capacity against the same controlled cluster. All tests use real endpoints; a one-node target remains a single-node cluster.
`wkcli bench` creates real users, Channels, connections, and messages and may drive a target into backpressure or unavailability. Use only an isolated, rebuildable, authorized benchmark cluster, with hard limits for rate, concurrency, duration, disk, and stopping.
## Command scope [#command-scope]
Prepare → validate → preflight → run.
Use fixed ceilings and stop gates.
Check delivery and recovery too.
Complete command scope
`wkcli bench` is WuKongIM's black-box benchmark driver. It talks to a running cluster through public HTTP, Benchmark HTTP, and WKProto gateway endpoints without importing server internals or bypassing cluster semantics. A one-node target is still a single-node cluster.
| Command | Purpose |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `validate` | Statically validate target, workers, and scenario YAML plus deterministic planning, without network checks |
| `doctor` | Check target health, Benchmark API, worker control APIs, and gateway reachability |
| `worker` | Start a worker control process that holds WKProto clients and executes workload shards |
| `run` | Execute validate, preflight, assign, prepare, connect, warmup, run, cooldown, and report |
| `dev-sim` | Keep users online and sustain low-rate person/group traffic for development |
| `capacity send` | Search maximum stable ingress send QPS against an existing cluster |
| `capacity hot-channel` | Search hot-write capacity for one fixed group Channel |
| `capacity activate-channels` | Activate and hold a fixed number of real Channel runtimes |
| `capacity message-event` | Apply fixed-shape `/message/event` pressure and write a report |
| `metrics classify` | Compare before/after Prometheus snapshots and emit low-cardinality attribution hints |
| `report` | Render benchmark evidence and diagnostic reports |
## Target prerequisites [#target-prerequisites]
Use an isolated, authorized target with reachable health/readiness, Benchmark, gateway and metrics endpoints. Enable `bench.api_enable` and `observability.metrics_enable`; never expose `/bench/v1/*` publicly. `capacity message-event` uses WuKongIM HTTP API without Benchmark API, but still writes generated data.
Full endpoint and configuration prerequisites
A complete workflow usually requires:
* `/healthz` and `/readyz`;
* `/bench/v1/capabilities`, `/bench/v1/capacity-target`, and `/bench/v1/snapshot`;
* Benchmark user, Channel, and subscriber preparation endpoints;
* a WKProto gateway advertised at an address reachable from the runner.
Explicitly enable the Benchmark API in a controlled environment:
```toml
[bench]
api_enable = true
```
`/bench/v1/*` is not a public integration API and must not be exposed to a public network. `capacity message-event` is the exception: it uses WuKongIM `/channel`, `/message/send`, `/message/event`, and `/metrics` endpoints and does not need the Benchmark API. It still writes generated Channels and messages, so it also requires a controlled target.
## Minimum validation flow [#minimum-validation-flow]
Download [target.yaml](/examples/wkbench/target.yaml), [workers.yaml](/examples/wkbench/workers.yaml), and [scenario.yaml](/examples/wkbench/scenario.yaml). This smoke workload has 20 online users and two 10-member groups sending 5 messages/second each. It measures 60 seconds, plus 10 seconds each for warmup and cooldown; it is not a production capacity claim.
**1. Prepare.** [Install wkcli](/en/server/tools/wkcli), edit the three YAML files and use reachable test endpoints. Terminal A runs the worker; Terminal B runs the coordinator. Both use the same worker token; the API token must match `bench.api_token`. Use a fresh `WK_BENCH_RUN_ID` and test-only credentials.
Download files and start Terminal A
First [install wkcli](/en/server/tools/wkcli). The following commands use the installed binary and do not require a source checkout or Go. Download the configurations into a working directory:
```bash
mkdir -p ./tmp/docs-wkbench
for file in target workers scenario; do
curl --fail --location "https://docs.githubim.com/examples/wkbench/${file}.yaml" \
--output "./tmp/docs-wkbench/${file}.yaml"
done
```
Edit `target.yaml` with the isolated test cluster's API, Gateway, and metrics endpoints. Defaults assume the test node, worker, and coordinator share a host; for separate hosts, make every endpoint reachable from the process that uses it.
Use the same worker control token in both terminals. The coordinator's API token must match `bench.api_token` on the test node, which also needs `bench.api_enable` and `observability.metrics_enable`. Do not use production credentials here.
Terminal A, start the worker:
```bash
export WK_BENCH_WORKER_TOKEN='replace-with-test-worker-secret'
wkcli bench worker \
--listen 127.0.0.1:19090 \
--work-dir ./tmp/docs-wkbench/worker
```
**2. Validate and preflight.** Terminal B runs `validate`, which never contacts the network, then `doctor`. On failure, fix configuration, tokens or endpoints before proceeding.
Terminal B: identity, validate and doctor
Terminal B, configure this run. Change `WK_BENCH_RUN_ID` for every run; do not reuse an existing identity:
```bash
export WK_BENCH_WORKER_TOKEN='replace-with-test-worker-secret'
export WK_BENCH_API_TOKEN='replace-with-test-api-secret'
export WK_BENCH_RUN_ID="docs-smoke-$(date -u +%Y%m%dT%H%M%SZ)"
wkcli bench validate \
--target ./tmp/docs-wkbench/target.yaml \
--workers ./tmp/docs-wkbench/workers.yaml \
--scenario ./tmp/docs-wkbench/scenario.yaml
```
`validate` does not access the network. After it passes, run preflight. If preflight fails, fix the target, tokens, or endpoints before running traffic:
```bash
wkcli bench doctor \
--target ./tmp/docs-wkbench/target.yaml \
--workers ./tmp/docs-wkbench/workers.yaml \
--scenario ./tmp/docs-wkbench/scenario.yaml
```
**3. Run and check.** Run only after preflight passes. Any connection, send-ack, receive-verification, worker error or P99 gate failure fails this example. Check exit code and report, then stop Terminal A with Ctrl+C. `keep_data` retains generated data; never delete a cluster data directory to clean up one run.
Full bounded run and result checks
Once preflight passes, execute the bounded workload:
```bash
wkcli bench run \
--target ./tmp/docs-wkbench/target.yaml \
--workers ./tmp/docs-wkbench/workers.yaml \
--scenario ./tmp/docs-wkbench/scenario.yaml
```
The example fails on any connection, send-acknowledgement, receive-verification, or worker error, and also fails when P99 exceeds the configured limits. Check the exit code and report, then stop Terminal A's worker with Ctrl+C. `cleanup.strategy=keep_data` retains generated users, groups, and messages. Handle them through your isolated test environment's retention plan; do not delete cluster data directories to clean up one run.
## Design a representative workload [#design-a-representative-workload]
Match user count, Channel count/types, members, payloads and reconnects to actual use. Keep warmup outside measurement; test hot Channels, high cardinality, events and connections separately. For 100,000-member groups, include fanout, bounded queues and resource budgets.
Full workload design checklist
* Model realistic online-user count, Channel cardinality and types, group size, payload size, acknowledgements, and reconnect behavior.
* Separate connection ramp, warmup, measurement, and cooldown; warmup counters must not enter the measured window.
* Test high Channel cardinality, one hot Channel, message events, and connection pressure separately rather than reducing every bottleneck to one QPS number.
* For 100,000-member groups, high message rates, many Channels, and many online users, account explicitly for CPU, memory, allocations, contention, bounded queues, backpressure, and fanout.
* Fix the random seed or generation rule and retain scenario YAML, tool/server revisions, hardware, topology, and configuration.
## Capacity search [#capacity-search]
`capacity send` starts a temporary local worker against an existing cluster; it does not manage cluster lifecycle or delete data. Use separate subcommands for hot Channels, runtime count and events. Results apply only to this version, hardware, topology, scenario and gates.
Complete capacity command and planning rules
```bash
wkcli bench capacity send \
--api http://127.0.0.1:5001 \
--profile mixed \
--start-qps 100 \
--max-qps 5000 \
--stable-p99 200ms \
--duration 30s \
--group-members 10
```
`capacity send` discovers gateways, starts a temporary local worker, and searches for a stable ingress rate under the supplied gates. It does not start or stop the cluster, build images, or remove data. Use separate `capacity` subcommands for a hot Channel, Channel-runtime cardinality, and message events.
“Maximum stable” applies only to this revision, hardware, topology, scenario, and gate. Plan below the first failure point with headroom. Actual QPS below offered QPS, tail latency, error rate, queues, disk, and recovery time are all results; never retain only the highest number.
## Results and stop conditions [#results-and-stop-conditions]
Stop on any hard gate, readiness, disk-headroom, error-rate, tail-latency or worker failure; do not raise limits to obtain a larger number. Retain versions/configs, full commands, stage times, offered/actual QPS, node resources, queue recovery and cleanup evidence. See [Diagnostics](/en/server/tools/diagnostics).
Full evidence and stop checklist
A report should retain at least:
* Git revision, binary digests, configuration, and cluster topology.
* Target/worker/scenario files and the complete command.
* Timing and status for prepare, connect, warmup, run, and cooldown.
* Offered/actual QPS, throughput, p50/p95/p99, typed operation errors, and timeouts.
* Per-node CPU, memory, goroutines, FDs, network, disk IO, queues, and replica/leader skew.
* Before/after `/readyz`, backlog recovery time, generated-data location, and cleanup result.
Stop on any hard gate, readiness, disk-headroom, error-rate, tail-latency, or worker-state failure. Do not raise limits merely to obtain a larger number. See [Diagnostics](/en/server/tools/diagnostics) for evidence selection.
## Cleanup [#cleanup]
Stop workers and simulators, revoke temporary credentials, disable the Benchmark API, archive reports, remove generated data, and verify the cluster returns to its idle baseline. If worker stop or target recovery cannot be confirmed, treat the run as an unfinished incident, not a successful benchmark.
## Read the report [#read-the-report]
Read `summary.md` and `report.json` under `run.report_dir`, then use `plan.json` and `metrics/` to explain the result. Record hardware separately before comparison. **Values below are illustrative, not measured.** Worker-local maximum P99 is not a merged percentile; sampling two recipients cannot prove full delivery. Passing send metrics with unrecovered queues is insufficient. JSON durations are nanoseconds; prefer formatted Markdown values.
Illustrative report and complete interpretation
In the report directory selected by `run.report_dir`, read `summary.md` and `report.json` first, then use the saved configurations, `plan.json`, and `metrics/` to explain the result. Before comparing runs, separately record server version or digest, SDK/tool version, CPU/memory/disk type per node, node count, replicas, and network conditions. Tool output does not replace a complete hardware inventory.
The following uses **illustrative values, not measured benchmark results**: the same server and tool versions, one 4-vCPU / 8-GiB / SSD single-node cluster with the default 256 hash slots, and the smoke scenario above.
| Report item | Illustrative observation | Interpretation |
| -------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Planned rate | 2 groups × 5 messages/second = 10/second | Target send rate, not achieved successful throughput |
| `summary.ingress_qps` | 9.8 | Below the target; inspect latency, errors, and scheduling |
| `summary.sendack_max_worker_p99` | 180 ms | Below the example 200-ms limit; the maximum worker-local P99, not a merged percentile across all messages |
| `summary.recv_verify_error_rate` | 0 | This example samples 2 recipients per message, so it does not prove delivery to every member |
| Delivery queue | Grows during measurement and remains after cooldown | Passing send metrics alone does not prove sustainable capacity |
Go `time.Duration` fields in JSON use nanoseconds; 180 ms is `180000000`. Prefer formatted latency values in `summary.md`. Increase load only after the measurement window, achieved throughput, errors, tail latency, recipient coverage, queue recovery, and resource headroom all satisfy your targets.
# wkcli (/en/server/tools/wkcli)
Install matching binaries, configure target addresses, then retain evidence with one Top snapshot and read-only node commands.
## Install and verify [#install-and-verify]
On Linux amd64, use APT / DNF via [Linux deployment](/en/server/deployment/linux). Linux arm64 and macOS use the matching release archive with checksum verification. Paired `wkcli` and `wukongim` must match `version`, `commit`, and `build_source`. Version checks need no running server.
Complete installation, verification, and version commands
`wkcli` is WuKongIM's unified operator CLI. Online operations use HTTP and WKProto entry points; `db` and `migrate` operate on offline data. Since **v3.0.0-beta.12**, official Linux/macOS archives and Linux native packages include both `wkcli` and `wukongim` from the same release.
On Linux amd64, follow [Linux deployment](/en/server/deployment/linux) to install the `wukongim` package through APT or DNF; it also installs `wkcli`. For Linux arm64 or macOS, download the matching OS and architecture archive from [GitHub Releases](https://github.com/WuKongIM/WuKongIM/releases), verify it against the release checksum file, and extract both binaries. The current documentation release is `v3.0.0-beta.24`. Add the extracted directory to `PATH`, or use `./wkcli` and `./wukongim` there.
```bash
wukongim version --output json
wkcli version --output json
wkcli bench --help
wkcli db --help
wkcli migrate --help
```
For the paired release binaries, `version`, `commit`, and `build_source` must match. These commands need no running server and do not start a benchmark or migration. The examples below assume `wkcli` is on `PATH`; see [Tools](/en/server/tools) for source builds.
## Command boundaries [#command-boundaries]
* **Online read-only:** `top`, `node ls`, and `node diagnose`; `context` saves only local target addresses.
* **Controlled writes:** node lifecycles retain Manager permissions, audit, and safety gates; they neither edit Controller / Slot directly nor manage server processes.
* **Other tasks:** `bench` / `sim` generate real traffic, `db import` writes offline storage, and `migrate` creates new offline data.
Old standalone entrypoints are removed. Use `wkcli bench`, `wkcli db`, and `wkcli migrate`; database global flags precede the operation.
Complete commands, risks, and old entrypoint mapping
| Command | Purpose | Risk class |
| ----------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------- |
| `bench` | [Distributed workloads, workers, capacity and reports](/en/server/tools/wkbench) | Generates real traffic |
| `db` | [Offline query, REPL, import, export and diff](/en/server/tools/wkdb) | Import writes the target store |
| `migrate` | [Original v2-to-v3 migration](/en/server/operations/v2-to-v3-migration) | Creates new offline v3 data |
| `context` | Save and select a set of WuKongIM HTTP API addresses | Writes only the local user configuration directory |
| `top` | Read and aggregate `/top/v1/snapshot` from one or more nodes | Online read-only |
| `node ls` / `node diagnose` | Read dynamic-node state and bounded root-cause evidence | Manager read-only |
| `node activate` / `node onboarding` / `node scale-in` | Advance a dynamic-node lifecycle | Controlled Manager writes |
| `bench send` | Run a lightweight SEND/SENDACK check | Generates real connections and messages |
| `sim` | Prepare test metadata and sustain real group-message traffic | Controlled simulation environments only |
`wkcli node` neither starts nor stops server processes and never writes Controller or Slot state directly. Manager permissions, audit, and safety gates still apply.
Replace old invocations `wkbench …`, `wkdb …`, and `wkmigrate …` with `wkcli bench …`, `wkcli db …`, and `wkcli migrate …`. Old executable entrypoints are removed. Command arguments, output formats and family-specific exit codes remain unchanged. Database global flags must precede its verb.
## Configure named contexts [#configure-named-contexts]
Configure absolute `http://` / `https://` API addresses. Repeat `--server` or comma-separate addresses. Recheck cluster identity and credential scope after selection; a context name does not grant approval.
Complete context configuration commands
```bash
wkcli context add dev \
--server http://127.0.0.1:5001 \
--server http://127.0.0.1:5002 \
--description "development cluster" \
--select
wkcli context ls
wkcli context show
wkcli context current
```
`--server` may be repeated or comma-separated, and every address must be an absolute `http://` or `https://` API URL. A context stores target addresses; selecting a context named production does not grant operational approval. Recheck cluster identity and credential scope before execution.
## Use Top [#use-top]
After configuring the context, run:
```bash
wkcli top --context dev \
--once --json
```
**Result:** retain the node snapshots and cross-check `/readyz`, Manager, and Prometheus. Aggregates are not globally consistent snapshots. Top needs no Prometheus and refreshes continuously by default; use `--once` or `--max-refresh` to bound scripts.
Complete Top output and refresh options
```bash
wkcli top --context dev --once
wkcli top --context dev --once --json
wkcli top --context dev --interval 2s --max-refresh 5
wkcli top --context dev --alerts
```
Top reads bounded node-local history and does not depend on Prometheus. It refreshes until interrupted by default; use `--once` or `--max-refresh` for scripts and incident evidence. Cross-check aggregates with `/readyz`, Manager, and Prometheus.
## Read dynamic-node evidence [#read-dynamic-node-evidence]
Use `node ls`, `node diagnose`, and `node scale-in status`. Keep health freshness, control revision, `blocked_reasons`, `safe_to_remove`, drain counters, and task / audit / Slot evidence. Missing data stays unknown.
Complete read-only dynamic-node commands
```bash
wkcli node ls --context dev
wkcli node diagnose 4 --context dev
wkcli node diagnose 4 --context dev --json
wkcli node scale-in status 4 --context dev
```
Retain health freshness, control revision, `blocked_reasons`, `safe_to_remove`, gateway drain counters, and bounded task/audit/Slot evidence. Missing data stays unknown; never infer healthy from an empty value.
## Node mutations [#node-mutations]
Run mutations only within [Scaling](/en/server/operations/scaling). Joining does not automatically assign Slot replicas or leaders; Controller voter changes require a separate decision. Removal needs authoritative `safe_to_remove=true`; diagnostic advice cannot replace it.
Complete node mutations and lifecycle gates
```bash
wkcli node activate 4 --context dev
wkcli node onboarding start 4 --context dev --max-slot-moves 1
wkcli node scale-in start 4 --context dev
wkcli node scale-in drain 4 --context dev --draining=true
wkcli node scale-in remove 4 --context dev
```
Run these only inside the explicit [Scaling](/en/server/operations/scaling) procedure. A dynamically joined node does not automatically receive Slot replicas or leaders, and Controller voter change is a separate explicit decision. Removal must wait for authoritative `safe_to_remove=true`; a CLI diagnostic recommendation does not replace that gate.
## Lightweight send checks [#lightweight-send-checks]
`bench send` creates real sessions and messages to check WKProto SEND throughput and SENDACK latency. Use dedicated test identities/channels and bound clients, messages, channels, and time. Use `wkcli bench` for full scenarios.
Complete lightweight send example
```bash
wkcli bench send \
--gateway 127.0.0.1:5100 \
--clients 8 \
--msgs 1000 \
--channels 10 \
--channel-prefix check-g \
--channel-type group \
--size 128B
```
`bench send` is a quick WKProto SEND-throughput and SENDACK-latency check, not a replacement for a complete `wkcli bench` scenario. It creates real sessions and messages. Bound clients, messages, channels, and runtime, and use dedicated test identities and channels.
## Long-running simulation [#long-running-simulation]
`sim` prepares metadata through Benchmark APIs and sends real `SEND → SENDACK` gateway traffic. Use only isolated development / benchmark clusters with explicitly enabled Benchmark API and a reachable advertised gateway.
`--rate` is per group; total rate grows with group count. Stop at the time limit, disable Benchmark API, and clean generated data according to the plan.
Complete bounded simulation and capacity path
`sim` prepares group metadata through `/bench/v1/*`, keeps users online through the real WKProto gateway, and sends `SEND -> SENDACK` traffic. The target must explicitly enable the Benchmark API and advertise a reachable gateway.
```bash
wkcli sim \
--server http://127.0.0.1:5001 \
--users 100 \
--groups 50 \
--group-members 10 \
--rate 0.25/s \
--max-runtime 30s
```
`--rate` is per group, so aggregate offered traffic grows with group count. Run only against an isolated development or benchmark cluster, then disable the Benchmark API and remove generated data. Use [wkcli bench](/en/server/tools/wkbench) for full validation, capacity search, and reports.
## Stop safely [#stop-safely]
Stop when identity, Manager permission, or authoritative state is unclear. Read task state after write timeouts before retrying. Stop Top and workloads at their budget. Check for simulators, temporary contexts, Benchmark API, and unfinished node tasks afterward.
Complete stopping checklist
* Do not run node mutations when target identity, Manager permission, or authoritative state is unclear.
* After a write request times out, read task state before retrying.
* Stop Top, send checks, and simulations when their time or traffic budget is reached.
* On exit, confirm there is no simulator, temporary context, exposed Benchmark API, or unfinished node task left behind.
# wkcli db (/en/server/tools/wkdb)
Verify one node’s data identity first. Keep read-only inspection and controlled import separate. The tool works on node files; it neither connects to the cluster nor reads global Controller / Raft / runtime state.
`info` only reads immutable identity and can run while the node is online. Other exact inspection should use a stopped node, filesystem snapshot, or copied data directory. Live files continue changing and cannot provide consistent evidence. `import` needs an explicitly offline target, a dry run, and a backup before writing.
## Operation classes [#operation-classes]
### Read the source [#read-the-source]
Queries only return results; diff compares two offline directories. Export reads the source and writes a separate output directory.
Only import writes WKDB. It neither joins a cluster nor migrates Controller / Raft state.
[Install wkcli](/en/server/tools/wkcli) first. Global flags follow `wkcli db` and precede the operation; operation-specific flags follow it.
Complete operations and read/write scope
`wkcli db` is a local offline tool for one WuKongIM node data directory. It never connects to cluster nodes and does not read global Controller, Raft, or runtime state. Begin with read-only inspection; only `import` writes WKDB storage.
Install [wkcli](/en/server/tools/wkcli) before using these examples. Database flags belong after `wkcli db` and before the operation.
| Command | Source access | Additional writes | View scope |
| ---------------- | ---------------------------------------- | -------------------------------------- | --------------------------------------------------------------- |
| `info` | Reads identity without opening databases | Standard output only | Node format and creator identity |
| `query` / `repl` | Read-only | Standard output only | One node's metadata and message stores |
| `export` | Read-only | Writes the `--output` bundle directory | Exportable bundle-v1 data from one node |
| `diff` | Both sides read-only | Standard output only | Bundle-v1 data differences between two offline node directories |
| `import` | Reads a bundle | Writes offline target WKDB | Data kinds supported by the bundle |
Global flags must precede the command; command-specific flags follow it.
## Inspect the data version [#inspect-the-data-version]
Use matching versions (`info` starts in `v3.0.0-beta.13`); replace the path with the actual node root:
```bash
wkcli db --data-dir ./node-1 \
info
```
**Result:** `registered` means valid identity and supported format; `unregistered` infers neither v2 nor v3; `unsupported` means the server rejects the format. Success verifies identity reading, not business data.
`info` opens no database or database lock, creates or writes no directory, and can run online. Missing paths or corrupt markers fail. Supply a node root or config without `--meta-path`, `--message-path`, or `--hash-slot-count`.
Current format is `wukongim-v3` / `2`. Format 1 and nonempty unregistered directories are rejected, without automatic migration. Never edit `DATA-FORMAT.json` to bypass checks; preserve it in full backups. Archive restore retains target creation identity; older binaries do not guarantee safe rollback.
Complete data-version commands, identity, and compatibility
This feature is available starting with v3.0.0-beta.13. Use matching server and tool versions.
```bash
wkcli db --data-dir ./node-1 info
wkcli db --data-dir ./node-1 --format json info
wkcli db --config ./wukongim.toml info
```
Every fresh empty node directory receives `DATA-FORMAT.json`. Migration targets receive it too, with the migration tool as their creator. `format` (currently `wukongim-v3`) and `format_version` (currently `2`) identify data compatibility. `created_by` records the program, version, commit, and build source; `created_at` records creation time. Ordinary software upgrades and repeated imports preserve these fields.
| Status | Meaning |
| -------------- | --------------------------------------------------------------------------- |
| `registered` | Valid identity with a format supported by this tool |
| `unregistered` | Directory exists without identity; neither v2/v3 nor creator is inferred |
| `unsupported` | Identity is readable, but the current server rejects this format at startup |
`info` opens no database or database lock, creates no directory, and writes nothing. It can run while a node is online. Missing paths and corrupt markers fail; a successful read is not verification of business data. Supply the node root or config, without `--meta-path`, `--message-path`, or `--hash-slot-count`.
Format 2 includes user/channel send policies and their versions. Format 1 and
nonempty unregistered data directories are rejected before opening stores; this
change does not migrate old data. Use matching server/CLI binaries and new or
explicitly imported compatible directories. Do not change the marker to bypass
the check. Preserve the marker in full directory backups; restoring an archive
keeps the target node's creation identity. Older binaries cannot use this marker
to guarantee safe rollback.
## Locate storage [#locate-storage]
Check node / cluster ID, copy time, version, and directory digests. `--data-dir` derives `slotmeta` and `messages`; paths can be overridden. `--config` reads TOML with `WK_` overrides. Physical hash slots are `256` and must match the source.
Complete storage paths and flags
```bash
wkcli db --data-dir ./node-1 --hash-slot-count 256 query "show tables"
wkcli db --config ./wukongim.toml query "select * from meta.user limit 20"
```
* `--data-dir` derives `slotmeta` and `messages` from the current node layout.
* `--meta-path` and `--message-path` explicitly override those paths.
* `--config` reads paths and hash-slot count from TOML; `WK_` environment variables still override file values.
* `--hash-slot-count` must match the cluster that produced the data. WuKongIM uses 256 physical hash slots.
Record node ID, cluster ID, copy/snapshot time, server version, and directory checksums so the wrong targets are not compared.
## Read-only queries [#read-only-queries]
A `uid` / `channel_id` partition key locates its hash slot; otherwise the tool scans this node with bounds. `limit` caps the whole result. Continue with the returned cursor; `offset` is unsupported. JSONL ends with stats containing `has_more` and `next_cursor`.
Complete query, cursor, and output examples
```bash
wkcli db --data-dir ./node-1 --hash-slot-count 256 \
query "select * from meta.user where uid='u1'"
wkcli db --data-dir ./node-1 \
query "select * from message.channels limit 20"
wkcli db --data-dir ./node-1 \
query "select * from message.message where channel_key='g1:2' limit 50"
```
With a partition key such as `uid` or `channel_id`, the tool derives its hash slot. Without a partition key it performs a bounded scan over this node's files. `limit` is the total result size, not a per-slot limit. Continue a large result with the returned cursor; `offset` is unsupported:
```bash
wkcli db --data-dir ./node-1 --hash-slot-count 256 \
query "select * from meta.user limit 100 cursor ''"
```
Select `--format table|json|jsonl`. JSONL emits data rows followed by a final stats object containing `has_more` and `next_cursor`.
## Export a bundle [#export-a-bundle]
Read-only source access produces a bundle-v1 manifest and JSONL with supported data, row counts, and SHA-256. It neither aggregates peers nor exports increments or online-consistent snapshots; it excludes Controller / Raft / runtime state and is not a Manager backup. Verify output paths before `--overwrite`.
Complete export commands and bundle scope
```bash
wkcli db --data-dir ./node-1 --hash-slot-count 256 \
export --output ./wkdb-dump
```
`export` opens the source read-only and writes only the output directory. WKDB Import Bundle v1 contains a manifest plus JSONL files for supported user, device, Channel, subscriber, ordinary membership, CMD membership, latest-sequence, and message data, with file row counts and SHA-256 digests.
It does not aggregate other nodes, create an online-consistent snapshot, export increments, include Controller/Raft/runtime state, or produce a Manager backup archive. Before adding `--overwrite`, resolve and verify the existing output path.
## Import a bundle [#import-a-bundle]
Use `--require-empty` for a new or deliberately empty offline target to prevent accidental merges. `--dry-run` opens no writable target. Preserve the target before writing. A one-node bundle does not become a complete cluster.
Complete dry-run and import commands
First validate without opening a writable target:
```bash
wkcli db --data-dir ./node-new --hash-slot-count 256 \
import --input ./wkdb-dump --dry-run
```
After validation, use a new or deliberately empty offline target:
```bash
wkcli db --data-dir ./node-new --hash-slot-count 256 \
import --input ./wkdb-dump --require-empty
```
`import` is the sole command that writes WKDB storage. It does not join a cluster, migrate Controller or Raft state, or turn a one-node bundle into a complete cluster. Preserve the target, confirm bundle version and hash-slot count, use `--require-empty` to avoid accidental merging, and run an offline diff before starting any node.
## Compare two offline directories [#compare-two-offline-directories]
`summary` compares rows and payload checksums; `full` also hashes message payload bytes. Equal data exits `0`; verified differences exit `2`. Retain stderr so read / configuration errors are not mistaken for data differences. Complete offline diff before starting nodes.
Complete summary / full diff commands
```bash
wkcli db --hash-slot-count 256 diff \
--source-data-dir ./node-old \
--target-data-dir ./node-new
wkcli db --hash-slot-count 256 diff \
--source-data-dir ./node-old \
--target-data-dir ./node-new \
--mode full
```
The default `summary` mode compares rows and payload checksums; `full` also hashes message payload bytes. Equal data exits 0 and a verified mismatch exits 2. Retain stderr with the exit code so a configuration or read error is not mistaken for a data mismatch.
## Cluster-restore boundary [#cluster-restore-boundary]
Use [Manager Backup & Restore](/en/server/operations/backup-and-restore) for cluster recovery: verify cluster identity, archive, tasks, and all `256` physical hash slots, then follow maintenance-mode atomic switching or rollback.
Complete node-transfer and cluster-restore boundaries
A `wkcli db` bundle is for node-local offline inspection and controlled transfer; it does not replace [Manager Backup & Restore](/en/server/operations/backup-and-restore). Cluster restore must also verify cluster identity, archives, tasks, and all 256 physical hash slots, then use the maintenance-mode atomic switch or rollback procedure.
# MQTT Authentication and Topics (/en/api/client-protocols/mqtt/authentication-and-topics)
The entry supports MQTT 5 TCP and WebSocket. Start with a development candidate containing MQTT; see the [quickstart](/en/guide/tutorials/mqtt).
## CONNECT identity [#connect-identity]
| Field | Value |
| ------------------------------ | ---------------------------------------------------------- |
| User Name | Exact UID registered by your business backend |
| Password | Existing device token matching the UID and device category |
| ClientID | Nonempty, stable session identifier; not a credential |
| User Property `wk.device_flag` | Exactly one string: `"0"` (APP), `"1"` (WEB) or `"2"` (PC) |
The device category is part of the credential. The Node.js example uses `"1"`, so register `device_flag=1` on the backend. It is not automatic runtime detection. Clients cannot select SYSTEM or DeviceLevel, or replace the token with a ClientID.
UIDs and ClientIDs are limited to 1024 UTF-8 bytes; tokens to 16 KiB. Whitespace-only identities are rejected; valid values are not automatically trimmed. Your production backend owns token issuance, rotation and revocation. Clients must not call WuKongIM HTTP API management directly.
```js
import mqtt from 'mqtt';
const client = mqtt.connect('mqtt://127.0.0.1:1883', {
protocolVersion: 5,
clientId: 'alice-device-01',
username: 'alice',
password: process.env.MQTT_ALICE_TOKEN,
clean: true,
reconnectPeriod: 0,
properties: {
sessionExpiryInterval: 0,
userProperties: { 'wk.device_flag': '1' },
},
});
client.on('error', () => console.error('MQTT connection failed'));
```
This is a connection-options fragment. The [quickstart](/en/guide/tutorials/mqtt) includes input validation, reception listeners and cleanup.
## Topics and authorization [#topics-and-authorization]
| Target | Topic | PUBLISH | SUBSCRIBE |
| ------ | ---------------------------- | ---------------------------------------------------------------- | -------------------------------------------- |
| Person | `wk/v1/users/{id}/messages` | Send to the target UID under existing direct-message permissions | Only that UID may subscribe to its own inbox |
| Group | `wk/v1/groups/{id}/messages` | Send to an existing group with membership and send permission | Existing membership required |
`{id}` is the business ID's UTF-8 bytes encoded as **canonical, unpadded base64url**. It is neither ordinary Base64 nor percent encoding.
```js
function topic(kind, id) {
return `wk/v1/${kind}/${Buffer.from(id, 'utf8').toString('base64url')}/messages`;
}
topic('users', 'bob'); // wk/v1/users/Ym9i/messages
topic('groups', 'g1'); // wk/v1/groups/ZzE/messages
```
The decoded ID must be nonempty, valid UTF-8, contain no NUL, and fit in 1024 bytes. Empty segments, trailing `=` and noncanonical encodings are rejected. Personal publication topics use the recipient UID, not an internal canonical person Channel ID.
SUBSCRIBE does not join a group; UNSUBSCRIBE does not leave it. A trusted backend creates groups and changes members. Membership and MQTT subscription are separate. Unsubscribe also does not cancel QoS 1 exchanges already begun; see [persistent sessions](/en/api/client-protocols/mqtt/sessions-and-qos).
## Reserved User Properties [#reserved-user-properties]
The server owns the `wk.` namespace. CONNECT accepts only `wk.device_flag` from clients; PUBLISH and Will accept only `wk.client_msg_no`. Duplicated reserved properties or forged properties such as `wk.from_uid` fail.
Unreserved User Properties may repeat, and the server preserves their order. A client library may represent repeated values as arrays; do not treat those as single strings. Every PUBLISH and Will requires exactly one `wk.client_msg_no`; see the [message contract](/en/api/client-protocols/mqtt/messages). See [Authentication](/en/guide/integration/authentication) for the business-backend trust boundary.
# MQTT (/en/api/client-protocols/mqtt)
MQTT is an IM entry protocol for WuKongIM. It shares UIDs, device tokens, channels, membership permissions and messages with HTTP and WKProto SDKs. Your application still owns login, business authorization and payload formats.
The MQTT listener is disabled by default and supports MQTT 5 TCP and an optional separate WebSocket listener. Complete Linux, fault and load qualification remains outstanding. Use a matching development candidate containing the MQTT implementation; do not assume current released packages or images support it.
## Start here [#start-here]
## Supported capabilities [#supported-capabilities]
| Capability | Current behavior |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Version and transport | MQTT 5.0 over raw TCP or WebSocket; other versions unsupported; TLS terminates upstream |
| Browsers | MQTT.js can connect directly to a separate `/mqtt` listener; the `mqtt` subprotocol and binary messages are required |
| Publish QoS | 0 and 1; no QoS 2 publication |
| Subscription QoS | 0 and 1; a request for 2 may be granted 1; inspect SUBACK |
| Topics | Exact personal inbox and group-message topics; no arbitrary business topics |
| Retain | Unsupported; Retain Available is 0 in CONNACK |
| Wildcards and shared subscriptions | No `+`, `#` or `$share/` |
| Topic Alias | Unsupported; Topic Alias Maximum is 0; send full topics |
| Authentication | CONNECT User Name / Password; no enhanced AUTH negotiation |
| Sessions | Clean Start, Session Expiry, Session Present; maximum offline lifetime 24 hours, configurable downward |
| Subscription options | No Local and Subscription Identifier supported; identifiers must be nonzero and not duplicated |
| Will | QoS 0/1 and Will Delay; no retain; current permissions checked at execution |
Inspect capabilities in CONNACK and each subscription's SUBACK. Client-library support does not establish server support.
## Integration model [#integration-model]
Your trusted backend prepares user tokens and group membership. Clients then connect and subscribe to exact topics. SUBSCRIBE establishes reception intent for a ClientID; it neither creates an account nor joins a group.
Successful PUBACK, SDK SENDACK and HTTP `reason=1` acknowledge message commit. None proves recipient reception, reading or business completion. Every deployment retains cluster semantics. A single-node cluster also uses Slots and durable paths, with 256 hash slots by default.
The [OASIS MQTT 5.0 specification](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html) defines the base protocol. This topic adds WuKongIM authentication, topic and IM mapping conventions.
# MQTT and HTTP / SDK Interoperability (/en/api/client-protocols/mqtt/interop)
MQTT requires a development candidate containing its implementation. WuKongIM HTTP API calls belong only in a trusted backend or protected test environment. Clients cannot manage users or membership directly.
## One message, three entry protocols [#one-message-three-entry-protocols]
| Entry | Personal target | Body | Successful commit acknowledgement |
| ------------- | ---------------------------------- | -------------------------------- | --------------------------------- |
| MQTT | `wk/v1/users/Ym9i/messages` (bob) | Raw IM bytes | Successful QoS 1 PUBACK |
| HTTP | `channel_id=bob`, `channel_type=1` | Ordinary Base64 of the raw bytes | Response `reason=1` |
| WKProto / SDK | Peer UID, person channel type 1 | SDK-encoded bytes | Successful SENDACK |
Groups use the group ID, channel type 2 and `wk/v1/groups/{base64url(groupID)}/messages`, under existing IM permissions. A personal inbox combines direct-message sources; identify original message channels using outbound properties.
Topic **base64url** and HTTP payload **ordinary Base64** are different encodings. Confusing them can reject a topic or corrupt its payload.
## HTTP → MQTT [#http--mqtt]
Prepare credentials through the [quickstart](/en/guide/tutorials/mqtt), then have Bob subscribe to `wk/v1/users/Ym9i/messages`. The trusted backend sends an SDK text message:
```bash
curl -sS http://127.0.0.1:5001/message/send \
-H 'Content-Type: application/json' \
-d '{
"from_uid":"alice",
"channel_id":"bob",
"channel_type":1,
"client_msg_no":"http-to-mqtt-0001",
"payload":"eyJ0eXBlIjoxLCJjb250ZW50IjoiaGVsbG8gQm9iIn0="
}'
```
Decoded Base64 is the UTF-8 bytes of `{"type":1,"content":"hello Bob"}`. Check HTTP `reason=1`, then independently inspect Bob's MQTT PUBLISH: identical bytes, `wk.from_uid=alice`, `wk.client_msg_no=http-to-mqtt-0001`, and MessageID identifying that commit. HTTP 2xx alone does not establish a successful business send.
## MQTT → SDK [#mqtt--sdk]
Bob logs in through an SDK and listens for messages. Alice publishes the same UTF-8 JSON bytes to Bob's topic with QoS 1, `retain=false` and an independent application number. The built-in text format is SDK-decodable. Custom JSON or binary formats require matching message types and decoders.
MQTT payloads do not contain encrypted WKProto frames or HTTP JSON request bodies. A raw `hello Bob` string is readable by MQTT clients, but does not guarantee SDK text rendering.
See [JavaScript Messaging](/en/sdk/javascript/messages) and [Custom Messages](/en/sdk/javascript/advanced/custom-messages). One UID can use distinct MQTT ClientIDs and a WKProto connection concurrently. MQTT does not evict another connection under WKProto master-device rules.
## Acceptance checks [#acceptance-checks]
For HTTP → MQTT, SDK → MQTT and MQTT → SDK, check commit, actual recipient content, sender and stable identity separately. Then check denied permissions, disconnects and duplicates. Persistent recovery requires the [session conditions](/en/api/client-protocols/mqtt/sessions-and-qos); online interoperability does not establish offline recovery for every SDK.
# MQTT Message Contract (/en/api/client-protocols/mqtt/messages)
This describes the MQTT 5 TCP / WebSocket development implementation. See the [overview](/en/api/client-protocols/mqtt) for capabilities and qualification boundaries.
## Inbound PUBLISH [#inbound-publish]
Publish to an exact personal or group topic with QoS 0 or 1, `retain=false`, and exactly one nonempty `wk.client_msg_no` User Property, limited to 1024 UTF-8 bytes.
```js
const payload = Buffer.from(JSON.stringify({ type: 1, content: 'hello Bob' }), 'utf8');
const clientMsgNo = 'alice-bob-0001'; // Preserve with the original body for an uncertain retry.
await client.publishAsync('wk/v1/users/Ym9i/messages', payload, {
qos: 1,
retain: false,
properties: { userProperties: { 'wk.client_msg_no': clientMsgNo } },
});
```
The payload is the existing IM message's raw bytes. The server adds no MQTT JSON envelope. This example uses the SDK text format. MQTT-only applications can define a binary format, but other SDKs need a matching decoder; see [interoperability](/en/api/client-protocols/mqtt/interop).
For a business retry of the same logical send, preserve `wk.client_msg_no`, target and body. Use a new number for a new message. Packet Identifier and DUP do not replace application idempotency. Do not interpret a timeout as proof of noncommit and resend under a new number.
## Success and uncertain outcomes [#success-and-uncertain-outcomes]
| Observation | What it establishes |
| -------------------------------------------- | ----------------------------------------------------------------------- |
| Successful QoS 1 PUBACK | Server confirmed durable commit and assumed responsibility |
| Failure PUBACK Reason Code | No successful confirmation; inspect permission or capacity failure |
| Timeout/disconnect without successful PUBACK | Outcome may be unknown; retain the original key and body |
| Recipient receives PUBLISH | Its connection received the message; not reading or business completion |
| Recipient sends PUBACK | QoS 1 protocol exchange completed; not an application read receipt |
An uncertain commit produces no successful PUBACK. QoS 0 has no PUBACK; its local publish callback cannot prove durable commit. QoS 0 also does not automatically set the IM NoPersist flag.
## Outbound User Properties [#outbound-user-properties]
| Property | Meaning |
| ------------------ | ------------------------------------------------------------------------------------------------------- |
| `wk.message_id` | Stable server MessageID, unchanged across reconnect or QoS 1 redelivery |
| `wk.message_seq` | Sequence within the message's channel |
| `wk.from_uid` | Authenticated sender UID |
| `wk.channel_id` | IM Channel identity; do not infer the original channel from a personal inbox topic |
| `wk.channel_type` | Current person/group mapping, as a decimal string |
| `wk.client_msg_no` | Original application number; see [Will messages](/en/api/client-protocols/mqtt/will) for Will semantics |
Numeric values are decimal strings. Never pass MessageID through JavaScript `Number` before storing it; large values lose precision.
```js
client.on('message', (topic, bytes, packet) => {
const props = packet.properties.userProperties;
const messageId = props['wk.message_id']; // Keep the decimal string.
// Persist/deduplicate by messageId before applying business effects.
});
```
This fragment does not acknowledge durable application processing. MQTT.js handles protocol ACKs by default. For reliable business processing, control ACK timing with the `handleMessage` completion callback and persist deduplication records. Returning a Promise from an asynchronous `message` listener does not defer protocol ACK.
## Ordering, expiry and size [#ordering-expiry-and-size]
`message_seq` orders one channel. A personal inbox combines multiple direct-message sources, including both directions of a person Channel, so a sender can receive its own projection. Use `wk.from_uid` to distinguish the sender and apply business roles; there is no cross-channel total order. QoS 1 can duplicate; deduplicate by stable MessageID, not Packet Identifier.
Message Expiry Interval limits validity, and remaining lifetime decreases during delivery and replay. Expired messages are not permanent offline archives. Ordinary history queries and MQTT session replay are separate interfaces.
The default inbound packet limit is 1 MiB including encoding overhead, leaving less space for payload. Publication metadata has a separate 32 KiB limit including identity and format overhead; excess is rejected without truncation. Outbound delivery also respects the client's Maximum Packet Size and Receive Maximum. See the [configuration reference](/en/server/configuration/reference).
# MQTT Operations and Troubleshooting (/en/api/client-protocols/mqtt/operations-and-troubleshooting)
MQTT is disabled by default. Complete Linux, fault and load qualification remains outstanding. Logical quotas are not guarantees of hardware capacity, throughput or concurrent users.
## Deployment requirements [#deployment-requirements]
Enable `mqtt.enable` on every MQTT ingress node, use matching candidate binaries/tools and keep namespace consistent. Namespace is a stable ClientID namespace; changing it arbitrarily is not session cleanup. Follow the candidate's cold-rollout constraints. Pre-integration MQTT development data is not a supported direct upgrade source; use pre-MQTT data or a separately verified migration.
A single-node cluster also uses 256 hash slots. Multi-node deployments still need Slot quorum and internal communication. Load balancing does not replace takeover isolation proof. A new CONNECT can fail when the old Owner cannot be proved stopped, even if other Slots retain quorum.
The default listener is `0.0.0.0:1883`; the tutorial restricts it to loopback. Public access needs upstream TLS termination and a protected plaintext backend network. `mqtts://` targets the TLS proxy; `mqtt://` targets raw TCP. Do not disable certificate verification. MQTT WebSocket uses a separate listener; the existing WKProto / JSON-RPC listener cannot accept MQTT packets.
Fields, environment variables and ranges are maintained in the [configuration reference](/en/server/configuration/reference). Keep the [authentication configuration](/en/server/configuration/security) as well.
## Browser WebSocket access [#browser-websocket-access]
Keep `mqtt.enable=true` and append this listener to your existing `gateway.listeners` list:
```toml
{ name = "mqtt-ws", network = "websocket", address = "127.0.0.1:1884", transport = "gnet", protocol = "mqtt", path = "/mqtt" }
```
This is one inline-table entry, not a complete TOML file. Use a unique address for each listener. `WK_GATEWAY_LISTENERS` replaces the entire list as JSON, so preserve the WKProto listeners you still need. MQTT WebSocket shares the same authentication, topics, limits and durable sessions as TCP; changing transport does not create a new MQTT namespace. It is not advertised by the IM `/route` response; your backend supplies its URL.
A browser application with the MQTT.js 5.16.0 package can connect with backend-issued credentials:
```js
import mqtt from 'mqtt';
const client = mqtt.connect('ws://127.0.0.1:1884/mqtt', {
protocolVersion: 5,
clientId: 'alice-browser-01',
username: credentials.uid,
password: credentials.token,
clean: true,
reconnectPeriod: 0,
properties: {
sessionExpiryInterval: 0,
userProperties: { 'wk.device_flag': '1' },
},
});
client.on('error', () => console.error('MQTT connection failed'));
```
`credentials` comes from your trusted business backend, which registers the corresponding WEB device token. Install reception listeners before publishing and use the topic and acknowledgement rules in the [quickstart](/en/guide/tutorials/mqtt). For persistent recovery, keep the ClientID stable and follow [sessions and QoS](/en/api/client-protocols/mqtt/sessions-and-qos).
MQTT.js offers the case-sensitive `mqtt` WebSocket subprotocol and sends binary data automatically. The listener returns `mqtt`, rejects missing or different subprotocols with HTTP 400, and rejects a wrong path with HTTP 404. Text data closes the connection; binary continuation frames, packets split across messages and multiple packets in one message are supported. Control ping/pong/close frames retain normal WebSocket behavior. These requirements follow [MQTT 5 section 6](https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html).
Use `wss://` behind your TLS proxy for HTTPS pages. Forward `/mqtt`, HTTP Upgrade and the subprotocol header; protect the plaintext backend listener. Native TLS and WebSocket compression are not provided by this transport.
## Backup and migration [#backup-and-migration]
The JSONL format used by `wkcli db export` does not yet support persistent MQTT state: ClientID/UID bindings, subscriptions, unfinished exchanges, Wills, shared replay and capacity records. Export refuses sources containing that state before creating or overwriting the output directory. Disabling MQTT or waiting for Session expiry does not remove this restriction.
Preserve the source and use native backup and restore with matching versions. Do not delete MQTT tables, replay or recovery records to bypass refusal; they may still retain delivery obligations or identity bindings.
## Quotas [#quotas]
| Boundary | Default | Operational meaning |
| ------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------- |
| Per-session logical backlog | 10000 messages / 64 MiB | Slow ACKs, offline delivery and replay retain responsibility; not physical disk usage |
| Durable QoS 1 window | 64 | Further limited by the client's Receive Maximum |
| Per-node shared storage reservation | 8 GiB | Logical reservation for shared originals and future replay copies |
| Aggregate cluster replica reservation | 64 GiB | Every storage node must agree, including nodes with MQTT listener disabled |
| Inbound packet | 1 MiB | Includes encoding overhead; client outbound size limit is separate |
| Offline session lifetime | At most 24 hours | Configurable downward; requests exceeding configured/hard limits fail |
A body shared by multiple sessions is counted once per retaining replica. Ordinary history, WAL, compaction and physical amplification are outside these logical limits. Exhaustion restricts new responsibility while preserving accepted replay and uncertain outcomes. Reservation retires only after proof; deleting data or closing a connection cannot establish reclamation.
For large groups, observe member count, online client count, message rate, ACK latency, backlog and disk headroom together. A 100,000-member group does not imply 100,000 online connections. Demonstrations or partial results cannot become general capacity guarantees.
## Diagnose by symptom [#diagnose-by-symptom]
| Symptom | First checks |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| TCP refusal/timeout | MQTT enabled, listener, TLS proxy and network; browser incorrectly using TCP |
| WebSocket handshake rejected | Exact path, case-sensitive `mqtt` subprotocol, TLS proxy Upgrade forwarding |
| CONNECT rejected | MQTT 5, UID/token/device category, ClientID, exactly one `wk.device_flag` |
| SUBACK rejection | Canonical base64url, own inbox, group membership, unsupported wildcards |
| Publication rejected/connection closes | `wk.client_msg_no`, reserved properties, retain/QoS, permission, size and quotas |
| Successful PUBACK but no reception | Receiver connection/subscription, permission, QoS, expiry, window and ACK; commit is not reception |
| Reconnect lacks subscriptions | Original UID/ClientID/namespace, `clean=false`, nonzero expiry, Session Present |
| Duplicate messages | Expected with QoS 1; inspect MessageID deduplication and changed application keys on retry |
| Missing Will | Normal DISCONNECT cancellation, delay/detection, current permission, generation, uncertain responsibility |
| CONNECT fails after partition | Old Owner isolation evidence, communication, Slot authority and recovery; avoid endless reconnect pressure |
Retain time bounds, node, redacted ClientID, topic, Reason Code, Session Present, MessageID and public metrics. Do not log tokens or sensitive payloads. Business retries follow the [idempotency contract](/en/api/client-protocols/mqtt/messages).
## Public observations [#public-observations]
With metrics enabled, inspect `/metrics` for `wukongim_mqtt_subscription_closures_total`, `wukongim_mqtt_storage_bytes`, `wukongim_mqtt_storage_events_total`, `wukongim_mqtt_owner_work` and `wukongim_mqtt_consumer_work`. Counters can count repeated attempts; they are not unique connection/message counts or reclamation proof.
Correlate metrics and client observations before reading bounded logs. See [Health and Monitoring](/en/server/operations/health-and-monitoring) and [Troubleshooting](/en/server/operations/troubleshooting).
# MQTT Persistent Sessions and QoS (/en/api/client-protocols/mqtt/sessions-and-qos)
Persistent sessions depend on authoritative cluster state. A new CONNECT may fail when isolation of the previous connection cannot be proved. Seamless takeover during arbitrary partitions is not guaranteed.
## Choose a lifecycle [#choose-a-lifecycle]
| Goal | ClientID | Clean Start | Session Expiry Interval |
| ----------------------------- | ------------------- | ------------------------------------------- | ---------------------------- |
| One exchange demonstration | May change each run | true | 0 |
| Create a persistent session | Reuse afterward | true, explicitly discarding the old session | Greater than 0 |
| Resume a persistent session | Original stable ID | false | Greater than 0 |
| Reset old subscriptions/state | Original ID | true | Choose for the new lifecycle |
The default offline lifetime ceiling is 86400 seconds (24 hours), configurable downward. Requests must not exceed the configured or hard ceiling. Inspect Session Present to establish actual recovery; `clean=false` alone does not prove old state exists.
A ClientID is UID-bound within a namespace. Distinct ClientIDs can coexist using one UID/device credential without the WKProto master-device eviction policy. A new connection with the same ClientID takes over the previous connection. Give different devices different ClientIDs.
## MQTT.js recovery options [#mqttjs-recovery-options]
This fragment resumes a persistent session. If none exists, Session Present is false and subscriptions must be created. Install message listeners before CONNECT so recovery delivery cannot arrive before your handler.
```js
import mqtt from 'mqtt';
const inbox = 'wk/v1/users/Ym9i/messages';
const client = mqtt.connect('mqtt://127.0.0.1:1883', {
manualConnect: true,
protocolVersion: 5,
clientId: 'bob-device-01', // Stable across reconnects; bound to bob.
username: 'bob',
password: process.env.MQTT_BOB_TOKEN,
clean: false,
resubscribe: false,
reconnectPeriod: 0, // Explicit reconnect here; production needs bounded backoff.
properties: {
sessionExpiryInterval: 3600,
receiveMaximum: 16,
userProperties: { 'wk.device_flag': '1' },
},
});
client.on('error', () => console.error('MQTT operation failed'));
client.on('message', (topic, bytes, packet) => {
// Deduplicate using packet.properties.userProperties['wk.message_id'].
});
client.on('connect', (connack) => {
if (!connack.sessionPresent) {
client.subscribe(inbox, { qos: 1 }, (error, grants) => {
if (error || grants?.[0]?.qos !== 1) console.error('Inbox subscription failed');
});
}
});
client.connect();
```
MQTT.js `clean` maps to Clean Start; `sessionExpiryInterval` is in seconds. Automatic resubscribe is disabled here to observe server recovery directly. Production clients need bounded retries, backoff and credential refresh rather than endless failed authentication. See the [MQTT.js API](https://github.com/mqttjs/MQTT.js/blob/v5.16.0/README.md#api).
## QoS and windows [#qos-and-windows]
QoS 1 is an at-least-once protocol exchange and still requires business deduplication. QoS 2 publication is unsupported. Subscription QoS and original publication QoS bound delivery. A QoS 1 subscription does not turn original QoS 0 into reliable offline delivery; do not promise reliable offline backlog for QoS 0.
The default durable QoS 1 window is 64, further limited by the client's Receive Maximum. Slow ACKs, offline clients and slow application processing can create backpressure. Inspect processing and logical backlog quotas before increasing the window.
Session expiry, Clean Start, explicit termination and permission changes affect recovery eligibility. UNSUBSCRIBE stops future reception for the subscription; it neither leaves a group nor cancels exchanges already begun. TCP closure does not immediately release uncertain responsibilities.
## Recovery checks [#recovery-checks]
1. Use a stable ClientID and nonzero expiry, subscribe to your own inbox or an existing group membership, and inspect SUBACK.
2. Disconnect the receiver. Within its lifetime, publish a QoS 1 message from another user, retaining the application number.
3. Reconnect with the original UID, ClientID and `clean=false`. Inspect Session Present; when true, do not SUBSCRIBE again.
4. Verify content, MessageID and channel sequence. Duplicate delivery must produce one business effect.
5. Test expiry and a Clean Start reset separately, detecting a new session and recreating required subscriptions.
These steps verify your client integration. They do not replace complete Linux, partition, recovery and load qualification, or guarantee recovery of expired messages. Complete the [quickstart](/en/guide/tutorials/mqtt) first.
# MQTT Will Messages (/en/api/client-protocols/mqtt/will)
Will supports QoS 0/1 and Will Delay, without retain. When cluster execution is uncertain, responsibility remains retained. Wall-clock time cannot establish that a message has been published.
## Configure a Will [#configure-a-will]
A Will is registered in CONNECT. It uses the same personal/group topics and existing IM payload bytes, with exactly one `wk.client_msg_no`.
```js
const will = {
topic: 'wk/v1/groups/ZzE/messages', // alice must already be a member of g1.
payload: Buffer.from(JSON.stringify({ type: 1, content: 'alice disconnected' }), 'utf8'),
qos: 1,
retain: false,
properties: {
willDelayInterval: 10,
userProperties: { 'wk.client_msg_no': 'alice-presence-0001' },
},
};
// Add will to the CONNECT options from the session example.
```
Use a stable ClientID and nonzero session expiry to check reconnection during the delay; see [persistent sessions](/en/api/client-protocols/mqtt/sessions-and-qos). A trusted backend prepares group membership. SUBSCRIBE cannot supply missing permission.
## Triggering and cancellation [#triggering-and-cancellation]
| Action | Check |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Abnormal disconnect or heartbeat timeout | Will may enter execution; detecting the failure takes time |
| Will Delay | Seconds; session ending and actual cluster processing also matter |
| Normal DISCONNECT (Reason Code 0) | Cancels this connection's Will |
| DISCONNECT with Will Message (Reason Code 4) | Explicitly requests publication; not a cancellation path |
| Resume the same session during the delay | Check cancellation of the old Will; the new Will belongs to a new connection generation |
| Permission revoked after disconnect | Current send permission is rechecked at execution; setup permission does not guarantee publication |
Ordinary MQTT.js `endAsync(false)` performs normal cleanup. Closing TCP, forcing process exit and normal DISCONNECT are different experiments. A Will disconnection signal cannot replace actual user presence, task results or business transactions.
## Numbers and deduplication [#numbers-and-deduplication]
The Will's `wk.client_msg_no` is metadata. The server executes with a generation-bound idempotency identity. Reusing a number cannot control another Will's ownership. Receivers still deduplicate by stable `wk.message_id`. Commit does not establish delivery to all subscribers; see [acknowledgement boundaries](/en/api/client-protocols/mqtt/messages).
## Verify your integration [#verify-your-integration]
1. Connect an authorized observer, subscribe to the target group and inspect SUBACK.
2. Register a Will from a valid member, disconnect abnormally, wait for detection and delay, and verify content, sender and MessageID.
3. Register a new Will and disconnect normally; verify the observer receives no Will for that connection.
4. Test reconnection within the delay and permission revocation, checking old-generation cancellation and current authorization separately.
Use bounded waits and independent application numbers. Preserve evidence for uncertain outcomes; do not clear sessions or storage to force success. See [operations and troubleshooting](/en/api/client-protocols/mqtt/operations-and-troubleshooting).
# Activate a Conversation (/en/api/product-http/conversations/activateConversation)
* **Caller**: Trusted backend only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/conversations/activate`
- Operation ID: `activateConversation`
Records an explicit open, switch, or resume action for ordering; message paths do not activate it.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/conversations/activate \
--header 'content-type: application/json' \
--data '{"uid":"alice","channel_id":"team-42","channel_type":2}'
```
#### Request schema — `ConversationMutationRequest`
Shared key for Conversation clear-unread, hide, or activate operations.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | yes | minLength: 1 | Unique identifier of the target user. |
| `channel_id` | `string` | yes | minLength: 1 | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Clear Conversation unread (/en/api/product-http/conversations/clearConversationUnread)
* **Caller**: Trusted backend only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/conversations/clearUnread`
- Operation ID: `clearConversationUnread`
Advances read_seq to the current committed head; unknown legacy fields such as message_seq are ignored.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/conversations/clearUnread \
--header 'content-type: application/json' \
--data '{"uid":"alice","channel_id":"team-42","channel_type":2}'
```
#### Request schema — `ConversationMutationRequest`
Shared key for Conversation clear-unread, hide, or activate operations.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | yes | minLength: 1 | Unique identifier of the target user. |
| `channel_id` | `string` | yes | minLength: 1 | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Hide Conversation through the current head (/en/api/product-http/conversations/hideConversation)
* **Caller**: Trusted backend only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/conversations/delete`
- Operation ID: `hideConversation`
Advances deleted_to_seq to the current committed head without deleting Channel membership.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/conversations/delete \
--header 'content-type: application/json' \
--data '{"uid":"alice","channel_id":"team-42","channel_type":2}'
```
#### Request schema — `ConversationMutationRequest`
Shared key for Conversation clear-unread, hide, or activate operations.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | yes | minLength: 1 | Unique identifier of the target user. |
| `channel_id` | `string` | yes | minLength: 1 | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Conversations (/en/api/product-http/conversations)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Synchronize a Conversation page (/en/api/product-http/conversations/listConversations)
* **Caller**: Trusted backend only
* **Scope**: limit bounds membership rows scanned, not the number of returned conversations; tombstones go to deletes. Previews use Leader-persisted data without runtime activation. Any read failure fails the whole page; retry the original request and cursor.
* **Success**: Only done=true completes a full pass and permits saving coverage; reset\_required=true requires rebuilding the local Conversation directory.
* **Recovery**: Previews include the latest tail edit; preserve the cursor on 503 unavailable. Content edits do not change conversation order or unread.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/conversation/list`
- Operation ID: `listConversations`
Reads Leader-persisted messages without runtime activation. Failed SENDs may appear. Any read error fails the page; retry the original cursor.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/conversation/list \
--header 'content-type: application/json' \
--data '{"uid":"alice","cursor":"","limit":50,"completed_coverage":0}'
```
#### Request schema — `ConversationListRequest`
Conversation-directory pagination parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | yes | minLength: 1 | User ID whose Conversation directory is read. |
| `cursor` | `string` | no | default: `""` | Opaque next_cursor from the previous page; omit it or send an empty string for the first page. |
| `limit` | `integer` | no | 0–200; default: `50` | Maximum membership entries scanned for this page; 0 uses the default 50 and the maximum is 200. |
| `completed_coverage` | `integer:int64` | no | 0–∞; default: `0` | Coverage saved from the client's latest pass that ended with done=true; send 0 for the first synchronization. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Canonical Conversation page |
| `400` | Input, validation, dependency, or downstream error |
| `500` | Channel or storage read failed; no partial page or cursor is returned. |
| `503` | Temporary read failure or restore maintenance; retry the same query with bounded backoff. |
#### Response schema (`200`) — `ConversationListResponse`
One canonical Conversation-directory page and its synchronization control state.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `conversations` | array<`ConversationListItem`> | yes | — | Resolved Conversations in this page. |
| `deletes` | array<`ConversationKey`> | yes | — | Conversation keys the client should delete. |
| `next_cursor` | `string` | no | — | Opaque cursor for the next page; return it unchanged. |
| `done` | `boolean` | yes | — | true means the full pass is complete; only then may coverage be saved. |
| `coverage` | `integer:int64` | yes | 0–∞ | Coverage version observed by the server for this synchronization pass. |
| `tombstones_retained_since` | `integer:int64` | yes | 0–∞ | Earliest coverage for which deletion tombstones are still retained. |
| `reset_required` | `boolean` | yes | — | true means client coverage predates the retention window and the local Conversation directory must be rebuilt. |
##### Referenced schema — `ConversationListItem`
Canonical Conversation state for one user.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | — | Conversation Channel ID. |
| `channel_type` | `integer` | yes | 1–255 | Conversation Channel type. |
| `active_at` | `integer:int64` | yes | — | Most recent Conversation activation time. |
| `read_seq` | `integer:uint64` | yes | 0–∞ | Greatest message sequence read by the user. |
| `deleted_to_seq` | `integer:uint64` | yes | 0–∞ | Greatest message sequence deleted by the user. |
| `unread` | `integer:uint64` | yes | 0–∞ | Unread count derived from the current last message and read_seq. |
| `last_message` | `ConversationLastMessage` | `null` | yes | — | Latest visible ordinary message persisted on the current Leader, regardless of SEND success; null when absent. |
##### Referenced schema — `ConversationLastMessage`
Last-message summary in the Conversation directory.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `message_id` | `integer:uint64` | yes | 0–∞ | Server int64 message ID. |
| `message_idstr` | `string` | yes | — | Decimal string mirror of message_id only. |
| `message_seq` | `integer:uint64` | yes | 0–∞ | Sequence of this message within the Channel. |
| `from_uid` | `string` | yes | — | Message sender UID. |
| `client_msg_no` | `string` | yes | — | Client message number. |
| `server_timestamp_ms` | `integer:int64` | yes | — | Server timestamp in milliseconds. |
| `payload` | `string | null` | yes | encoding: base64 | Base64-encoded message payload; it may be null. |
| `version` | `string` | no | pattern: `^[0-9]+$` | Edited content version; absent means zero in the same content epoch. |
| `updated_at_ms` | `integer:int64` | no | 0–∞ | Server edit time in Unix milliseconds; original ordering is unchanged. |
##### Referenced schema — `ConversationKey`
Channel key for one Conversation.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1 | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`500`)
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | no | — | — |
| `status` | `integer` | no | — | — |
#### Response schema (`503`)
##### Referenced schema — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
##### Referenced schema — `MessageUpdateError`
Edit or transient read failure; no partial results.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | — | HTTP status. |
| `code` | `string` | yes | — | Stable rejection code; uncertain writes retain their exact retry identity. |
| `msg` | `string` | no | — | Optional legacy diagnostic message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Set maximum Conversation unread (/en/api/product-http/conversations/setConversationUnread)
* **Caller**: Trusted backend only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/conversations/setUnread`
- Operation ID: `setConversationUnread`
Monotonically advances read_seq so no more than unread messages remain.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/conversations/setUnread \
--header 'content-type: application/json' \
--data '{"uid":"alice","channel_id":"team-42","channel_type":2,"unread":3}'
```
#### Request schema — `ConversationSetUnreadRequest`
Parameters for setting a Conversation maximum unread count.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | yes | minLength: 1 | Unique identifier of the target user. |
| `channel_id` | `string` | yes | minLength: 1 | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
| `unread` | `integer` | yes | 0–∞ | Maximum unread messages to retain; it must be at least 0 and the server only advances read_seq monotonically. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Synchronize legacy Conversations (/en/api/product-http/conversations/syncConversationsLegacy)
* **Caller**: Trusted backend only
* **Scope**: Recents contain the latest edits; an edited tail is returned again even when last\_msg\_seqs is caught up. Merge by message ID and version.
* **Success**: The legacy array, conversation version, unread, ordering, only\_unread and msg\_count=0 behavior are preserved.
* **Recovery**: Transient reads return 503 unavailable; preserve the request and cache and retry with backoff. Reset message version comparisons when the content epoch increases.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/conversation/sync`
- Operation ID: `syncConversationsLegacy`
Read persisted heads and recents without Channel activation. Failed SENDs may appear; any read error fails the request. Legacy fields and cursors are preserved.
#### Request schema — `ConversationSyncLegacyRequest`
Legacy sync parameters. Retry the original request on failure. List and sync share 16 active requests and a five-second deadline; at most 1,000 candidates, 200 Channels per batch, 10,000 requested recents and 32 MiB of base/stream payloads. Budget exhaustion fails the whole request.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | yes | minLength: 1; pattern: `\S` | Non-blank user ID whose legacy Conversations are synchronized. |
| `version` | `integer:int64` | no | default: `0` | Conversation version known by the client; 0 disables version filtering. |
| `last_msg_seqs` | `string` | no | default: `""` | Pipe-delimited channel_id:channel_type:message_seq cursors; malformed entries are skipped. |
| `msg_count` | `integer` | no | default: `0` | Recent messages read per Conversation; values at or below 0 return an empty array and positive values are capped at 10000. |
| `only_unread` | `integer:uint8` | no | 0–255; default: `0` | Unread-filter compatibility flag; exactly 1 selects unread Conversations only. |
| `exclude_channel_types` | `array | null` | no | — | Channel types to exclude; it may be omitted, empty, or null, and an explicit Channel cursor overrides this filter. |
| `page` | `integer` | no | default: `0` | Legacy page number; values at or below 0 disable legacy page slicing. |
| `page_size` | `integer` | no | default: `100` | Legacy page size; when page>0, values at or below 0 use 100 and values above 500 are capped at 500. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Bare legacy Conversation array |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Temporary read failure or restore maintenance; retry the same query with bounded backoff. |
#### Response schema (`200`)
##### Referenced schema — `ConversationSyncLegacyItem`
v2.2-compatible Conversation projection.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | — | Conversation Channel ID. |
| `channel_type` | `integer:uint8` | yes | 0–255 | Conversation Channel type. |
| `unread` | `integer:uint64` | yes | 0–∞ | Legacy unread count. |
| `timestamp` | `integer:int64` | yes | — | Legacy Conversation timestamp. |
| `last_msg_seq` | `integer:uint64` | yes | 0–∞ | Current last-message sequence. |
| `last_client_msg_no` | `string` | yes | — | Last-message client number; old empty-number records use the same read-only alias as recents. |
| `offset_msg_seq` | `integer:int64` | yes | — | Offset sequence for this recent-message window. |
| `readed_to_msg_seq` | `integer:uint64` | yes | 0–∞ | Message sequence read by the user. |
| `version` | `integer:int64` | yes | — | Legacy Conversation version. |
| `recents` | array<`LegacyMessage`> | yes | — | Recent messages bounded by msg_count. To repair old empty-key collisions, the latest empty-number message or edited tail may be returned again after the client cursor advances; read and delete positions are unchanged. |
##### Referenced schema — `LegacyMessage`
Legacy message projection used by WuKongIM HTTP API synchronization.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `header` | `LegacyMessageHeader` | yes | additionalProperties: `false` | Legacy message header. |
| `setting` | `integer:uint8` | yes | 0–255 | WKProto Setting bitset. |
| `message_id` | `integer:int64` | yes | — | Server int64 message ID. |
| `message_idstr` | `string` | yes | — | Decimal string mirror of message_id only; it is not a replacement for message_seq. |
| `client_msg_no` | `string` | yes | — | Original client number, or the stable read-only alias wk3-legacy- for an old empty-number record with a message ID. The alias supports /messages lookup; it is not a SEND idempotency or event mutation key. Stored records are unchanged. |
| `end` | `integer:uint8` | no | 0–255 | Stream-message terminal compatibility flag. |
| `end_reason` | `integer:uint8` | no | 0–255 | Stream-message terminal Reason Code. |
| `error` | `string` | no | — | Stream-message error description. |
| `stream_data` | `string | null` | no | encoding: base64 | Base64-encoded stream data; it may be null. |
| `event_meta` | `LegacyMessageEventMeta` | no | additionalProperties: `false` | Optional message-event summary. |
| `event_sync_hint` | `LegacyMessageEventSyncHint` | no | additionalProperties: `false` | Optional message-event continuation hint. |
| `message_seq` | `integer:uint64` | yes | 0–∞ | uint64 sequence of the message within this Channel. |
| `from_uid` | `string` | yes | — | Message sender UID. |
| `channel_id` | `string` | yes | — | Channel ID containing the message. |
| `channel_type` | `integer:uint8` | yes | 0–255 | Channel type containing the message. |
| `topic` | `string` | no | — | Optional message topic. |
| `expire` | `integer:uint32` | yes | 0–∞ | Message expiration in seconds; 0 means unset. |
| `timestamp` | `integer:int32` | yes | — | Legacy server timestamp in seconds. |
| `payload` | `string | null` | yes | encoding: base64 | Base64-encoded message payload; it may be null. |
| `version` | `string` | no | pattern: `^[0-9]+$` | Edited content version; absent means zero in the same content epoch. |
| `updated_at_ms` | `integer:int64` | no | 0–∞ | Server edit time in Unix milliseconds; original ordering is unchanged. |
##### Referenced schema — `LegacyMessageHeader`
Legacy message header.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `no_persist` | `integer` | yes | enum: `0`, `1` | 1 means the message is non-durable. |
| `red_dot` | `integer` | yes | enum: `0`, `1` | 1 means the client should show a red-dot indicator. |
| `sync_once` | `integer` | yes | enum: `0`, `1` | 1 means a one-shot command message. |
##### Referenced schema — `LegacyMessageEventMeta`
Optional message-event summary.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `has_events` | `boolean` | yes | — | Whether events exist. |
| `completed` | `boolean` | yes | — | Whether all known event lanes have completed. |
| `event_version` | `integer:uint64` | no | 0–∞ | Event-summary version. |
| `last_msg_event_seq` | `integer:uint64` | no | 0–∞ | Latest event sequence across all lanes. |
| `event_count` | `integer` | no | 0–∞ | Total event count. |
| `open_event_count` | `integer` | no | 0–∞ | Number of event lanes still open. |
| `events` | array<`LegacyMessageEventKeyMeta`> | no | — | Per-lane event summaries. |
##### Referenced schema — `LegacyMessageEventKeyMeta`
Summary of one event lane.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `event_key` | `string` | yes | — | Event lane key. |
| `status` | `string` | yes | — | Current state of the lane. |
| `last_msg_event_seq` | `integer:uint64` | no | 0–∞ | Latest known event sequence for the lane. |
| `snapshot` | `unknown` | no | — | Arbitrary JSON snapshot in full summary mode. |
| `end_reason` | `integer:uint8` | no | 0–255 | Event-stream terminal Reason Code. |
| `error` | `string` | no | — | Event-stream error description. |
##### Referenced schema — `LegacyMessageEventSyncHint`
Optional message-event continuation hint.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `client_msg_no` | `string` | yes | — | Client message number of the target message. |
| `from_msg_event_seq` | `integer:uint64` | yes | 0–∞ | Starting sequence for the next event synchronization. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`)
##### Referenced schema — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
##### Referenced schema — `MessageUpdateError`
Edit or transient read failure; no partial results.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | — | HTTP status. |
| `code` | `string` | yes | — | Stable rejection code; uncertain writes retain their exact retry identity. |
| `msg` | `string` | no | — | Optional legacy diagnostic message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Add Channel allowlist members (/en/api/product-http/channels/addChannelAllowlistMembers)
* **Caller**: Trusted backend only
* **Scope**: The entry does not verify that the parent Channel exists; derived allowlist state can be created first and become effective if the parent Channel appears later.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/whitelist_add`
- Operation ID: `addChannelAllowlistMembers`
Adds non-blank UIDs to the derived Channel allowlist.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend or operator boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/channel/whitelist_add \
--header 'content-type: application/json' \
--data '{"channel_id":"team-42","channel_type":2,"uids":["moderator"]}'
```
#### Request schema — `ChannelAllowlistMutationRequest`
Allowlist add or remove parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1; pattern: `^[^#@]+$` | Target Channel ID; it must not be empty or contain # or @. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
| `uids` | array<`string`> | yes | minItems: 1 | One or more non-blank UIDs to add or remove. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Add Channel denylist members (/en/api/product-http/channels/addChannelDenylistMembers)
* **Caller**: Trusted backend only
* **Scope**: The entry does not verify that the parent Channel exists; derived denylist state can be created first and become effective if the parent Channel appears later.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/blacklist_add`
- Operation ID: `addChannelDenylistMembers`
Adds the supplied UID strings to the derived Channel denylist.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend or operator boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/channel/blacklist_add \
--header 'content-type: application/json' \
--data '{"channel_id":"team-42","channel_type":2,"uids":["blocked-user"]}'
```
#### Request schema — `ChannelMemberMutationRequest`
Denylist add or remove parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1 | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
| `uids` | array<`string`> | yes | minItems: 1 | One or more UIDs to add or remove; entry does not validate element content. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Add or replace durable subscribers (/en/api/product-http/channels/addChannelSubscribers)
* **Caller**: Trusted backend only
* **Scope**: A missing target Channel is created implicitly; an omitted or zero channel\_type is treated as group type 2.
* **Atomicity**: Reset removal, chunked insertion, and large refresh are separate stages and may partially complete.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/subscriber_add`
- Operation ID: `addChannelSubscribers`
Adds non-blank subscribers; channel_type 0 becomes group type 2 and reset=1 replaces the snapshot.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend or operator boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/channel/subscriber_add \
--header 'content-type: application/json' \
--data '{"channel_id":"team-42","channel_type":2,"reset":0,"temp_subscriber":0,"subscribers":["carol"]}'
```
#### Request schema — `ChannelSubscriberAddRequest`
Ordinary-subscriber add or replacement parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1; pattern: `^(?!.*[#@])(?=.*\S).+$` | Target non-blank Channel ID; it must not contain # or @. |
| `channel_type` | `integer:uint8` | no | 0–255; default: `2`; not: `1` | Target Channel type; omission or 0 becomes group type 2, while type 1 is rejected. |
| `reset` | `integer` | no | default: `0` | Subscriber-snapshot replacement compatibility flag; exactly 1 enables it and every other integer disables it. |
| `temp_subscriber` | `integer` | no | not: `1` | Deprecated temporary-subscriber flag; value 1 is rejected and every other value is ignored. |
| `subscribers` | array<`string`> | yes | minItems: 1 | One or more non-blank subscriber UIDs to add. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Terminally disband a Channel (/en/api/product-http/channels/disbandChannel)
* **Caller**: Protected operator boundary only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/delete`
- Operation ID: `disbandChannel`
Sets the durable disband flag without deleting Channel identity; request-key validation is weak.
#### Request schema — `WeakChannelKeyRequest`
Compatibility Channel key with deliberately weak validation.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | no | default: `""` | Channel ID to disband; omission produces an empty string and the entry does not validate it. |
| `channel_type` | `integer:uint8` | no | 0–255; default: `0` | Channel type to disband; 1 is rejected while every other value, including 0, is passed to the use case. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Read channel send restriction (/en/api/product-http/channels/getChannelSendBan)
* **Caller**: Trusted backend only
* **Scope**: Restriction for the actual source Channel; person IDs must be canonical and CMD-derived keys are invalid; login, receiving and protocol ACKs remain allowed.
* **Success**: Reads current Channel Slot authority; a missing person Channel returns allow/version 0, a missing non-person Channel returns 404.
* **Recovery**: On 503, keep the policy unknown and retry reads with backoff; unavailable authority is not evidence of allow.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `GET` `/channel/send_ban`
- Operation ID: `getChannelSendBan`
Read one current Slot-owned send policy without returning credentials.
#### Parameters
| Name | In | Required | Type | Constraints | Description |
| --- | --- | --- | --- | --- | --- |
| `channel_id` | query | yes | `string` | minLength: 1; maxLength: 1024; pattern: `^\S(?:[\s\S]*\S)?$` | Actual source Channel ID, at most 1024 UTF-8 bytes; person IDs must be canonical and CMD-derived IDs are rejected. |
| `channel_type` | query | yes | `integer` | 1–255 | Source Channel type; 1 denotes a canonical person Channel, 2 a group. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Authoritative policy / 权威策略 |
| `400` | Invalid body, identity, flag or version / 无效请求 |
| `404` | Non-person Channel not found / 非个人 Channel 不存在 |
| `503` | Authority unavailable or restore maintenance / 权威不可用或恢复维护 |
#### Response schema (`200`) — `ChannelSendBanResponse`
Policy response. Versions are independent for UID and source Channel.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | HTTP status repeated in the body. |
| `data` | `object` | yes | additionalProperties: `false` | Authoritative policy without credential fields. |
#### Response schema (`400`) — `SendBanError`
Policy error envelope; maintenance uses the separate MaintenanceError envelope.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | enum: `400`, `404`, `409`, `503` | HTTP failure status. |
| `code` | `string` | yes | enum: `"invalid_request"`, `"channel_not_found"`, `"version_conflict"`, `"channel_disbanded"`, `"version_exhausted"`, `"temporarily_unavailable"` | Typed policy failure; no current policy or version is included. |
| `msg` | `string` | yes | — | Error message; invalid input may use the generic invalid send ban request message. |
#### Response schema (`503`)
##### Referenced schema — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Channels (/en/api/product-http/channels)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List all Channel allowlist members (/en/api/product-http/channels/listChannelAllowlistMembers)
* **Caller**: Protected operator boundary only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `GET` `/channel/whitelist`
- Operation ID: `listChannelAllowlistMembers`
Returns an unbounded full list; missing or invalid channel_type is silently treated as 0.
#### Parameters
| Name | In | Required | Type | Constraints | Description |
| --- | --- | --- | --- | --- | --- |
| `channel_id` | query | no | `string` | default: `""` | Channel ID whose allowlist is read; this legacy endpoint does not validate the query parameter. |
| `channel_type` | query | no | `integer` | 0–255; default: `0` | Channel type whose allowlist is read; parsed as uint8, with missing, invalid, or out-of-range text silently treated as 0. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Complete legacy allowlist member array |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `ChannelMemberListResponse`
##### Referenced schema — `ChannelMember`
Member in the legacy allowlist response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `id` | `integer:uint64` | yes | 0–∞ | Internal member-record ID; do not use it as the UID. |
| `uid` | `string` | yes | — | Unique identifier of the target user. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Remove all Channel allowlist members (/en/api/product-http/channels/removeAllChannelAllowlistMembers)
* **Caller**: Trusted backend only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/whitelist_remove_all`
- Operation ID: `removeAllChannelAllowlistMembers`
Clears the derived allowlist through bounded internal traversal.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend or operator boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/channel/whitelist_remove_all \
--header 'content-type: application/json' \
--data '{"channel_id":"team-42","channel_type":2}'
```
#### Request schema — `ChannelKeyRequest`
Channel key with basic validation.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1 | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Remove all Channel denylist members (/en/api/product-http/channels/removeAllChannelDenylistMembers)
* **Caller**: Trusted backend only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/blacklist_remove_all`
- Operation ID: `removeAllChannelDenylistMembers`
Clears the derived denylist through bounded internal traversal.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend or operator boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/channel/blacklist_remove_all \
--header 'content-type: application/json' \
--data '{"channel_id":"team-42","channel_type":2}'
```
#### Request schema — `ChannelKeyRequest`
Channel key with basic validation.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1 | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Remove all durable subscribers (/en/api/product-http/channels/removeAllChannelSubscribers)
* **Caller**: Trusted backend only
* **Scope**: Clears ordinary subscribers and then refreshes large; person Channels are not supported.
* **Atomicity**: Removal and large refresh are not one transaction.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/subscriber_remove_all`
- Operation ID: `removeAllChannelSubscribers`
Clears ordinary subscribers through bounded internal pages; person Channels are rejected.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend or operator boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/channel/subscriber_remove_all \
--header 'content-type: application/json' \
--data '{"channel_id":"team-42","channel_type":2}'
```
#### Request schema — `NonPersonChannelKeyRequest`
Non-person Channel key.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1 | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer:uint8` | yes | 2–255 | Non-person Channel type, so the accepted range is 2–255. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Remove Channel allowlist members (/en/api/product-http/channels/removeChannelAllowlistMembers)
* **Caller**: Trusted backend only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/whitelist_remove`
- Operation ID: `removeChannelAllowlistMembers`
Removes non-blank UIDs from the derived Channel allowlist.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend or operator boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/channel/whitelist_remove \
--header 'content-type: application/json' \
--data '{"channel_id":"team-42","channel_type":2,"uids":["moderator"]}'
```
#### Request schema — `ChannelAllowlistMutationRequest`
Allowlist add or remove parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1; pattern: `^[^#@]+$` | Target Channel ID; it must not be empty or contain # or @. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
| `uids` | array<`string`> | yes | minItems: 1 | One or more non-blank UIDs to add or remove. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Remove Channel denylist members (/en/api/product-http/channels/removeChannelDenylistMembers)
* **Caller**: Trusted backend only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/blacklist_remove`
- Operation ID: `removeChannelDenylistMembers`
Removes the supplied UID strings from the derived Channel denylist.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend or operator boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/channel/blacklist_remove \
--header 'content-type: application/json' \
--data '{"channel_id":"team-42","channel_type":2,"uids":["blocked-user"]}'
```
#### Request schema — `ChannelMemberMutationRequest`
Denylist add or remove parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1 | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
| `uids` | array<`string`> | yes | minItems: 1 | One or more UIDs to add or remove; entry does not validate element content. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Remove durable subscribers (/en/api/product-http/channels/removeChannelSubscribers)
* **Caller**: Protected operator boundary only
* **Scope**: Removes existing members without implicitly creating the Channel; channel\_type=0 is passed through unchanged.
* **Atomicity**: Member removal and large refresh are separate stages and may partially complete.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/subscriber_remove`
- Operation ID: `removeChannelSubscribers`
Removes non-blank subscribers; unlike subscriber_add, channel_type 0 is not normalized.
#### Request schema — `ChannelSubscriberRemoveRequest`
Ordinary-subscriber removal parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1; pattern: `^(?!.*[#@])(?=.*\S).+$` | Target non-blank Channel ID; it must not contain # or @. |
| `channel_type` | `integer:uint8` | no | 0–255; default: `0`; not: `1` | Target Channel type; type 1 is rejected, while omission or 0 is passed through without normalization. |
| `reset` | `integer` | no | default: `0` | Integer flag accepted for compatibility but not used by the removal operation. |
| `temp_subscriber` | `integer` | no | — | Legacy temporary-subscriber flag accepted and ignored for compatibility. |
| `subscribers` | array<`string`> | yes | minItems: 1 | One or more non-blank subscriber UIDs to remove. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Replace Channel allowlist members (/en/api/product-http/channels/setChannelAllowlistMembers)
* **Caller**: Protected operator boundary only
* **Scope**: The allowlist uses full-replacement semantics.
* **Atomicity**: The list is removed and then inserted; the two stages are not transactional.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/whitelist_set`
- Operation ID: `setChannelAllowlistMembers`
Removes the old list before adding supplied values; only channel_id is validated at entry.
#### Request schema — `ChannelMemberSetRequest`
Complete allowlist or denylist replacement parameters with legacy weak validation.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1; pattern: `\S` | Target non-blank Channel ID. |
| `channel_type` | `integer:uint8` | no | 0–255; default: `0` | Target Channel type; it may be omitted or 0 and is not rejected at entry. |
| `uids` | `array | null` | no | — | Complete replacement UID set; it may be omitted, empty, or null, and elements are not validated at entry. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Replace Channel denylist members (/en/api/product-http/channels/setChannelDenylistMembers)
* **Caller**: Protected operator boundary only
* **Scope**: The denylist uses full-replacement semantics.
* **Atomicity**: The list is removed and then inserted; the two stages are not transactional.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/blacklist_set`
- Operation ID: `setChannelDenylistMembers`
Removes the old list before adding the supplied values; only channel_id is validated at entry.
#### Request schema — `ChannelMemberSetRequest`
Complete allowlist or denylist replacement parameters with legacy weak validation.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1; pattern: `\S` | Target non-blank Channel ID. |
| `channel_type` | `integer:uint8` | no | 0–255; default: `0` | Target Channel type; it may be omitted or 0 and is not rejected at entry. |
| `uids` | `array | null` | no | — | Complete replacement UID set; it may be omitted, empty, or null, and elements are not validated at entry. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Set channel send restriction (/en/api/product-http/channels/setChannelSendBan)
* **Caller**: Trusted backend only
* **Scope**: Restriction for the actual source Channel; person IDs must be canonical and CMD-derived keys are invalid; login, receiving and protocol ACKs remain allowed.
* **Atomicity**: The policy and its independent uint64 version change atomically in one Slot apply; an identical value does not increment the version.
* **Success**: 200 returns the applied policy with a decimal-string version; terminal disband is irreversible.
* **Recovery**: A timeout or 503 may follow a commit; GET the current policy first. After 409, reload and make a new mutation decision instead of blindly retrying the old expected\_version.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/send_ban`
- Operation ID: `setChannelSendBan`
Atomically set one send policy with optional decimal-string CAS; unchanged values retain the version.
#### Request schema — `ChannelSendBanRequest`
One strict JSON object, at most 4096 bytes. Unknown fields and trailing JSON are rejected; numeric fields use Go integer syntax.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1; maxLength: 1024; pattern: `^\S(?:[\s\S]*\S)?$` | Actual source Channel ID, at most 1024 UTF-8 bytes; person IDs must be canonical and CMD-derived IDs are rejected. |
| `channel_type` | `integer` | yes | 1–255 | Source Channel type; 1 denotes a canonical person Channel, 2 a group. |
| `uid` | `string | null` | no | enum: `""`, `null` | Must be absent, empty or null for a Channel policy. |
| `send_ban` | `integer` | yes | enum: `0`, `1` | Required integer flag: 1 restricts application SEND, 0 clears it. Login, receiving and protocol ACKs remain allowed. |
| `expected_version` | `string | null` | no | pattern: `^(?:0\|[1-9][0-9]{0,18}\|1[0-7][0-9]{18}\|18[0-3][0-9]{17}\|184[0-3][0-9]{16}\|1844[0-5][0-9]{15}\|18446[0-6][0-9]{14}\|184467[0-3][0-9]{13}\|1844674[0-3][0-9]{12}\|184467440[0-6][0-9]{10}\|1844674407[0-2][0-9]{9}\|18446744073[0-6][0-9]{8}\|1844674407370[0-8][0-9]{6}\|18446744073709[0-4][0-9]{5}\|184467440737095[0-4][0-9]{4}\|18446744073709550[0-9]{3}\|18446744073709551[0-5][0-9]{2}\|1844674407370955160[0-9]{1}\|1844674407370955161[0-4]\|18446744073709551615)$` | Optional apply-time CAS version; omitted or null means unconditional. Overflow, leading zeros and signs are rejected. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Authoritative policy / 权威策略 |
| `400` | Invalid body, identity, flag or version / 无效请求 |
| `404` | Non-person Channel not found / 非个人 Channel 不存在 |
| `409` | Version conflict/exhaustion or Channel disbanded / 版本冲突、耗尽或 Channel 已解散 |
| `503` | Authority unavailable or restore maintenance / 权威不可用或恢复维护 |
#### Response schema (`200`) — `ChannelSendBanResponse`
Policy response. Versions are independent for UID and source Channel.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | HTTP status repeated in the body. |
| `data` | `object` | yes | additionalProperties: `false` | Authoritative policy without credential fields. |
#### Response schema (`400`) — `SendBanError`
Policy error envelope; maintenance uses the separate MaintenanceError envelope.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | enum: `400`, `404`, `409`, `503` | HTTP failure status. |
| `code` | `string` | yes | enum: `"invalid_request"`, `"channel_not_found"`, `"version_conflict"`, `"channel_disbanded"`, `"version_exhausted"`, `"temporarily_unavailable"` | Typed policy failure; no current policy or version is included. |
| `msg` | `string` | yes | — | Error message; invalid input may use the generic invalid send ban request message. |
#### Response schema (`503`)
##### Referenced schema — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Replace temporary Channel subscribers (/en/api/product-http/channels/setTemporaryChannelSubscribers)
* **Caller**: Trusted backend only
* **Scope**: Temporary subscribers use full-replacement semantics.
* **Atomicity**: The service removes all members before chunked insertion; failure may leave only part of the new set.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/tmpchannel/subscriber_set`
- Operation ID: `setTemporaryChannelSubscribers`
Replaces the derived Type-8 temporary subscriber list; UID elements are not validated by the entry adapter.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend or operator boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/tmpchannel/subscriber_set \
--header 'content-type: application/json' \
--data '{"channel_id":"ephemeral-42","uids":["alice","bob"]}'
```
#### Request schema — `TemporarySubscriberSetRequest`
Complete temporary-subscriber snapshot.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1; pattern: `^[^#@]+$` | Temporary Channel ID; it must not be empty or contain # or @. |
| `uids` | array<`string`> | yes | minItems: 1 | Replacement temporary-subscriber UIDs; at least one is required and entry does not validate element content. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Replace Channel metadata (/en/api/product-http/channels/updateChannelInfo)
* **Caller**: Protected operator boundary only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/info`
- Operation ID: `updateChannelInfo`
Passes the zero-valued full record to UpdateInfo when fields are omitted; retained for compatibility.
#### Request schema — `ChannelInfoRequest`
Legacy Channel metadata; other fields use zero-value replacement while omitted or null send_ban preserves its current value.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | no | default: `""` | Channel ID to update; omission produces an empty string and the entry does not validate it. |
| `channel_type` | `integer:uint8` | no | 0–255; default: `0` | Channel type to update; omission produces 0 and the entry does not validate it. |
| `large` | `integer` | no | default: `0` | Large-group compatibility flag; exactly 1 enables it and every other integer disables it. |
| `ban` | `integer` | no | default: `0` | Channel-ban compatibility flag; exactly 1 enables it and every other integer disables it. |
| `disband` | `integer` | no | default: `0` | Terminal-disband compatibility flag; exactly 1 enables it and every other integer disables it. |
| `send_ban` | `integer | null` | no | enum: `0`, `1`, `null` | Optional strict 0/1 send restriction; omission or null preserves the current value, explicit 0 clears it. |
| `allow_stranger` | `integer` | no | default: `0` | Allow-stranger compatibility flag; exactly 1 enables it and every other integer disables it. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Create or update Channel metadata (/en/api/product-http/channels/upsertChannel)
* **Caller**: Trusted backend only
* **Scope**: Channel metadata and membership are written to cluster storage; large is recomputed from the current member count after membership changes.
* **Atomicity**: Metadata update, old-member removal, chunked member insertion, and large refresh are separate stages, not one transaction.
* **Recovery**: After a timeout or 400/5xx, replay the desired business state and verify membership through the protected Manager query.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel`
- Operation ID: `upsertChannel`
Upserts all metadata flags and optionally resets subscribers; disband is terminal.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend or operator boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/channel \
--header 'content-type: application/json' \
--data '{"channel_id":"team-42","channel_type":2,"reset":1,"subscribers":["alice","bob"]}'
```
#### Request schema — `ChannelUpsertRequest`
Full Channel metadata update plus optional subscriber replacement parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1; pattern: `^(?!.*[#@])(?=.*\S).+$` | Non-blank Channel ID to create or update; it must not contain # or @. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
| `large` | `integer` | no | default: `0` | Large-group compatibility flag; exactly 1 enables it and every other integer disables it. |
| `ban` | `integer` | no | default: `0` | Channel-ban compatibility flag; exactly 1 enables it and every other integer disables it. |
| `disband` | `integer` | no | default: `0` | Terminal-disband compatibility flag; exactly 1 enables it and every other integer disables it. |
| `send_ban` | `integer | null` | no | enum: `0`, `1`, `null` | Optional strict 0/1 send restriction; omission or null preserves the current value, explicit 0 clears it. |
| `allow_stranger` | `integer` | no | default: `0` | Allow-stranger compatibility flag; exactly 1 enables it and every other integer disables it. |
| `reset` | `integer` | no | default: `0` | Subscriber-snapshot replacement compatibility flag; exactly 1 enables it and every other integer disables it. |
| `subscribers` | `array | null` | no | — | Ordinary subscriber UIDs to add; it may be omitted, empty, or null, a person Channel rejects a non-empty list, and elements are not validated at entry. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Message Sending (/en/api/product-http/message-send)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Submit a message (/en/api/product-http/message-send/sendChannelMessage)
* **Caller**: Trusted backend only
* **Scope**: Messages are submitted by a trusted backend; optional X-WK-Trace-ID must be 32 hexadecimal characters or the server ignores it and generates a new value.
* **Success**: Even with HTTP 200, inspect reason; only a success Reason Code means the message was accepted.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/message/send`
- Operation ID: `sendChannelMessage`
Accepts the complete compatibility parser, including legacy aliases, transient flags, and request-scoped subscribers; inspect reason on HTTP 200.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/message/send \
--header 'content-type: application/json' \
--data '{"from_uid":"system","channel_id":"alice","channel_type":1,"client_msg_no":"order-20260828-0001","payload":"eyJ0eXBlIjoib3JkZXJfdXBkYXRlIn0="}'
```
#### Parameters
| Name | In | Required | Type | Constraints | Description |
| --- | --- | --- | --- | --- | --- |
| `X-WK-Trace-ID` | header | no | `string` | pattern: `^[0-9a-fA-F]{32}$` | Optional 32-character hexadecimal trace ID; an invalid value is ignored and replaced with a generated ID. |
#### Request schema — `SendMessageRequest`
Complete compatibility message-send parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `from_uid` | `string` | no | — | Message sender UID; when empty, sender_uid is consulted, and when both are empty the system account configured by message.system_uid is used. |
| `sender_uid` | `string` | no | — | Deprecated sender-UID alias used only when from_uid is empty; when both are empty, the configured system account is used. |
| `device_id` | `string` | no | — | Caller-supplied sending-device identifier; it may be empty. |
| `channel_id` | `string` | no | — | Target Channel ID; required for an ordinary send and required to be empty for a request-scoped subscribers send. |
| `channel_type` | `integer:uint8` | no | 0–255 | Target Channel type; it must be greater than 0 for an ordinary send and is 0 for a request-scoped subscribers send. |
| `client_msg_no` | `string` | no | — | Optional client idempotency key; omitted or blank values generate a unique number returned in the response. Reuse an explicit key when retrying the same message. |
| `setting` | `integer:uint8` | no | 0–255 | WKProto Setting bitset; combine SDK constants instead of writing unknown bits by hand. |
| `topic` | `string` | no | — | Optional Topic; it has protocol meaning only when the matching Setting bit and client capability are present. |
| `expire` | `integer:uint32` | no | 0–∞ | Caller-supplied message expiration in seconds; 0 means no expiration is set. |
| `payload` | `string` | yes | minLength: 1; encoding: base64 | Base64-encoded message payload; decoding failure returns 400. |
| `subscribers` | `array | null` | no | — | Request-scoped delivery UIDs; when non-empty, channel_id must be empty and sync_once must be enabled; the field may be omitted, empty, or null. |
| `header` | `SendMessageHeaderRequest` | `null` | no | — | Optional compatibility header flags; it may be an object, null, or omitted. |
| `no_persist` | `integer` | no | default: `0` | Top-level compatibility flag; non-zero means non-durable and is ORed with header.no_persist. |
| `sync_once` | `integer` | no | default: `0` | Top-level compatibility flag; non-zero means a one-shot command message and is ORed with header.sync_once. |
##### Referenced schema — `SendMessageHeaderRequest`
Compatibility flags for the message fixed header; same-named top-level no_persist and sync_once fields are ORed with the header values.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `red_dot` | `integer` | no | default: `0` | Non-zero preserves the message red-dot flag; omitted means 0. There is no top-level red_dot alias. |
| `no_persist` | `integer` | no | default: `0` | A non-zero value makes the message non-durable. |
| `sync_once` | `integer` | no | default: `0` | A non-zero value treats the message as a one-shot command message. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Transport completed; inspect reason |
| `400` | Invalid request or payload |
| `404` | Channel not found |
| `408` | Request cancelled or timed out |
| `500` | Unmapped send failure |
| `503` | Retry-required routing failure or restore maintenance |
#### Response schema (`200`) — `SendMessageResponse`
Completed message-send transport response; reason still requires inspection.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `message_id` | `integer:int64` | yes | — | Server message ID; JavaScript must parse it without losing int64 precision. |
| `message_seq` | `integer:uint64` | yes | 0–∞ | Sequence of this message within the target Channel; JavaScript must parse it without losing uint64 precision. |
| `reason` | `integer:uint8` | yes | enum: `1`, `2`, `3`, `4`, `5`, `9`, `11`, `13`, `15`, `18`, `19`, `24`, `25` | WKProto Reason Code; HTTP 200 does not replace this success check. |
| `client_msg_no` | `string` | yes | — | Client message number submitted with this send; generated by the server when omitted. |
#### Response schema (`400`) — `SendError`
HTTP error body for message sending.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | — | Human-readable diagnostic text; do not match the full text. |
#### Response schema (`503`)
##### Referenced schema — `RetryRequiredError`
Send error requiring re-routing before retry.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"retry required"` | Fixed at retry required. |
##### Referenced schema — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Resolve one address group for UIDs (/en/api/product-http/routing/getGatewayRoutesBatch)
* **Caller**: Trusted backend only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/route/batch`
- Operation ID: `getGatewayRoutesBatch`
Echoes an unbounded UID array in one address group; retained for compatibility. Address completion follows GET /route.
#### Parameters
| Name | In | Required | Type | Constraints | Description |
| --- | --- | --- | --- | --- | --- |
| `node_id` | query | no | `integer:uint64` | 1–∞ | Preferred node selector; when supplied it must identify a configured node with an ID greater than 0. |
| `nodeId` | query | no | `integer:uint64` | 1–∞ | Legacy nodeId alias, read only when node_id is absent. |
| `nodeID` | query | no | `integer:uint64` | 1–∞ | Legacy nodeID alias, read only when both node_id and nodeId are absent. |
| `intranet` | query | no | `integer` | default: `0` | Address-scope selector; any parseable non-zero integer selects intranet addresses, while invalid text behaves as 0 and selects public addresses. |
#### Request schema — `RouteBatchRequest`
Array of UID strings; JSON null is also accepted as an empty array, and the current entry does not bound its size.
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | One selected address group |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `RouteBatchResponse`
##### Referenced schema — `RouteBatchItem`
A group of UIDs sharing one Gateway address set.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `tcp_addr` | `string` | yes | — | WKProto TCP ingress address. |
| `ws_addr` | `string` | yes | — | Non-TLS WebSocket ingress address. |
| `wss_addr` | `string` | yes | — | TLS WebSocket ingress address. |
| `uids` | array<`string`> | no | — | UIDs assigned to this address set; compatibility responses may omit it. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Resolve Gateway addresses (/en/api/product-http/routing/getQuickstartGatewayRoute)
* **Caller**: Trusted backend only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `GET` `/route`
- Operation ID: `getQuickstartGatewayRoute`
Default external routes complete wildcard listener hosts from Host; explicit addresses win. See [address rules](/en/server/configuration/reference).
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend boundary.
curl --fail-with-body http://127.0.0.1:5001/route
```
#### Parameters
| Name | In | Required | Type | Constraints | Description |
| --- | --- | --- | --- | --- | --- |
| `node_id` | query | no | `integer:uint64` | 1–∞ | Preferred node selector; when supplied it must identify a configured node with an ID greater than 0. |
| `nodeId` | query | no | `integer:uint64` | 1–∞ | Legacy nodeId alias, read only when node_id is absent. |
| `nodeID` | query | no | `integer:uint64` | 1–∞ | Legacy nodeID alias, read only when both node_id and nodeId are absent. |
| `intranet` | query | no | `integer` | default: `0` | Address-scope selector; any parseable non-zero integer selects intranet addresses, while invalid text behaves as 0 and selects public addresses. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Selected Gateway addresses |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `RouteResponse`
Client ingress addresses for the selected Gateway.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `tcp_addr` | `string` | yes | — | WKProto TCP ingress address. |
| `ws_addr` | `string` | yes | — | Non-TLS WebSocket ingress address. |
| `wss_addr` | `string` | yes | — | TLS WebSocket ingress address. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Route Discovery (/en/api/product-http/routing)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Add system UIDs (/en/api/product-http/users/addSystemUIDs)
* **Caller**: Protected operator boundary only
* **Scope**: The UID set is persisted first, then only the handling process permission cache is updated. This request does not refresh other nodes.
* **Recovery**: Refresh the local cache on every server node, or roll the nodes, so effective permission state matches the persisted set.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/user/systemuids_add`
- Operation ID: `addSystemUIDs`
Persists system identities and adds them to the current process cache.
#### Request schema — `SystemUIDsRequest`
System-identity mutation parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uids` | `array | null` | no | — | System UIDs to add or remove; the field may be omitted, empty, or null, and elements are not validated at entry. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Add node-local system UID cache entries (/en/api/product-http/users/addSystemUIDsToLocalCache)
* **Caller**: Protected current-node operator boundary only
* **Scope**: Changes only the handling process in-memory cache; it is neither persisted nor broadcast to other nodes.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/user/systemuids_add_to_cache`
- Operation ID: `addSystemUIDsToLocalCache`
Mutates only the current process cache and does not persist or replicate the change.
#### Request schema — `SystemUIDsRequest`
System-identity mutation parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uids` | `array | null` | no | — | System UIDs to add or remove; the field may be omitted, empty, or null, and elements are not validated at entry. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Read user send restriction (/en/api/product-http/users/getUserSendBan)
* **Caller**: Trusted backend only
* **Scope**: Global application-send restriction for one UID; login, receiving and protocol ACKs remain allowed.
* **Success**: Reads current UID Slot authority; an unknown UID returns allow/version 0.
* **Recovery**: On 503, keep the policy unknown and retry reads with backoff; unavailable authority is not evidence of allow.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `GET` `/user/send_ban`
- Operation ID: `getUserSendBan`
Read one current Slot-owned send policy without returning credentials.
#### Parameters
| Name | In | Required | Type | Constraints | Description |
| --- | --- | --- | --- | --- | --- |
| `uid` | query | yes | `string` | minLength: 1; maxLength: 512; pattern: `^\S(?:[\s\S]*\S)?$` | UID with no surrounding whitespace; runtime limits its UTF-8 encoding to 512 bytes. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Authoritative policy / 权威策略 |
| `400` | Invalid body, identity, flag or version / 无效请求 |
| `503` | Authority unavailable or restore maintenance / 权威不可用或恢复维护 |
#### Response schema (`200`) — `UserSendBanResponse`
Policy response. Versions are independent for UID and source Channel.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | HTTP status repeated in the body. |
| `data` | `object` | yes | additionalProperties: `false` | Authoritative policy without credential fields. |
#### Response schema (`400`) — `SendBanError`
Policy error envelope; maintenance uses the separate MaintenanceError envelope.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | enum: `400`, `404`, `409`, `503` | HTTP failure status. |
| `code` | `string` | yes | enum: `"invalid_request"`, `"channel_not_found"`, `"version_conflict"`, `"channel_disbanded"`, `"version_exhausted"`, `"temporarily_unavailable"` | Typed policy failure; no current policy or version is included. |
| `msg` | `string` | yes | — | Error message; invalid input may use the generic invalid send ban request message. |
#### Response schema (`503`)
##### Referenced schema — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Users (/en/api/product-http/users)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List all system UIDs (/en/api/product-http/users/listSystemUIDs)
* **Caller**: Protected operator boundary only
* **Scope**: Returns the durable system-UID set; it does not prove that every node currently has the same in-memory permission cache.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `GET` `/user/systemuids`
- Operation ID: `listSystemUIDs`
Aggregates the complete persisted system UID set into one unbounded response.
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Complete system UID list |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `SystemUIDListResponse`
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# List active user routes (/en/api/product-http/users/listUserOnlineStatus)
* **Caller**: Protected operator boundary only
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/user/onlinestatus`
- Operation ID: `listUserOnlineStatus`
Returns one row per active authority route; an empty UID array returns the legacy status object instead of an array.
#### Request schema — `OnlineStatusRequest`
Array of UID strings; JSON null or an empty array returns the legacy status object, and the current entry does not bound its size.
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Active routes or the empty-request status object |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `OnlineStatusResponse`
##### Referenced schema — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
##### Referenced schema — `UserOnlineStatus`
One active authoritative device route for a user.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | yes | — | Unique identifier of the target user. |
| `device_flag` | `integer:uint8` | yes | 0–255 | Online device category. |
| `online` | `integer` | yes | const: `1` | Fixed at 1 to indicate that this route is online. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Clear a user device token (/en/api/product-http/users/quitUserDevice)
* **Caller**: Protected operator boundary only
* **Scope**: The handling node performs session closure after about 2 seconds; device\_flag=-1 processes APP, Web, and PC sequentially.
* **Atomicity**: Multi-device sign-out is not transactional; earlier device classes may be signed out before a later one fails.
* **Success**: A missing target device still returns 200.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/user/device_quit`
- Operation ID: `quitUserDevice`
Clears one stored device token and schedules owner-local Session closure; device_flag -1 selects APP, Web, and PC.
#### Request schema — `DeviceQuitRequest`
Device sign-out parameters with legacy weak validation.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | no | default: `""` | User ID whose device is signed out; the entry itself does not reject an empty value. |
| `device_flag` | `integer` | no | default: `0` | Device category to sign out; -1 selects APP, Web, and PC together, while other integers are converted to uint8 by the use case. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Remove system UIDs (/en/api/product-http/users/removeSystemUIDs)
* **Caller**: Protected operator boundary only
* **Scope**: UIDs are removed from durable state first, then only from the handling process permission cache.
* **Recovery**: Refresh the local cache on every server node, or roll the nodes.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/user/systemuids_remove`
- Operation ID: `removeSystemUIDs`
Removes persisted system identities and current-process cache entries.
#### Request schema — `SystemUIDsRequest`
System-identity mutation parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uids` | `array | null` | no | — | System UIDs to add or remove; the field may be omitted, empty, or null, and elements are not validated at entry. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Remove node-local system UID cache entries (/en/api/product-http/users/removeSystemUIDsFromLocalCache)
* **Caller**: Protected current-node operator boundary only
* **Scope**: Changes only the handling process in-memory cache; it is neither persisted nor broadcast to other nodes.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/user/systemuids_remove_from_cache`
- Operation ID: `removeSystemUIDsFromLocalCache`
Mutates only the current process cache and does not change durable system identities.
#### Request schema — `SystemUIDsRequest`
System-identity mutation parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uids` | `array | null` | no | — | System UIDs to add or remove; the field may be omitted, empty, or null, and elements are not validated at entry. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Create or update a device token (/en/api/product-http/users/setQuickstartUserToken)
* **Caller**: Trusted backend only
* **Scope**: The token is persisted cluster-wide; for a Master device, the handling node starts closing old sessions after about 10 seconds.
* **Success**: A 200 response means the token write completed, not that old sessions are already closed.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/user/token`
- Operation ID: `setQuickstartUserToken`
Upserts one UID/device token; default Gateway authentication requires later CONNECT credentials for the same UID and device category to match it.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/user/token \
--header 'content-type: application/json' \
--data '{"uid":"alice","token":"server-generated-development-secret","device_flag":1,"device_level":0}'
```
#### Request schema — `UpdateTokenRequest`
Device-token update parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | yes | minLength: 1 | User ID whose device token is created or updated; it must not contain @, #, or &. |
| `token` | `string` | yes | minLength: 1 | Opaque token persisted for this user and device category; the default Gateway requires later CONNECT credentials to match it exactly. |
| `device_flag` | `integer:uint8` | no | 0–255; default: `0` | Device category; 0=APP, 1=Web, 2=PC, and 99=system, defaulting to 0 when omitted. |
| `device_level` | `integer:uint8` | no | 0–255; default: `0` | Same-category device conflict level; 0=Slave and 1=Master, defaulting to 0 when omitted. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Set user send restriction (/en/api/product-http/users/setUserSendBan)
* **Caller**: Trusted backend only
* **Scope**: Global application-send restriction for one UID; login, receiving and protocol ACKs remain allowed.
* **Atomicity**: The policy and its independent uint64 version change atomically in one Slot apply; an identical value does not increment the version.
* **Success**: 200 returns the applied policy and a decimal-string version.
* **Recovery**: A timeout or 503 may follow a commit; GET the current policy first. After 409, reload and make a new mutation decision instead of blindly retrying the old expected\_version.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/user/send_ban`
- Operation ID: `setUserSendBan`
Atomically set one send policy with optional decimal-string CAS; unchanged values retain the version.
#### Request schema — `UserSendBanRequest`
One strict JSON object, at most 4096 bytes. Unknown fields and trailing JSON are rejected; numeric fields use Go integer syntax.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | yes | minLength: 1; maxLength: 512; pattern: `^\S(?:[\s\S]*\S)?$` | UID with no surrounding whitespace; runtime limits its UTF-8 encoding to 512 bytes. |
| `channel_id` | `string | null` | no | enum: `""`, `null` | Must be absent, empty or null for a user policy. |
| `channel_type` | `integer | null` | no | enum: `0`, `null` | Must be absent, zero or null for a user policy. |
| `send_ban` | `integer` | yes | enum: `0`, `1` | Required integer flag: 1 restricts application SEND, 0 clears it. Login, receiving and protocol ACKs remain allowed. |
| `expected_version` | `string | null` | no | pattern: `^(?:0\|[1-9][0-9]{0,18}\|1[0-7][0-9]{18}\|18[0-3][0-9]{17}\|184[0-3][0-9]{16}\|1844[0-5][0-9]{15}\|18446[0-6][0-9]{14}\|184467[0-3][0-9]{13}\|1844674[0-3][0-9]{12}\|184467440[0-6][0-9]{10}\|1844674407[0-2][0-9]{9}\|18446744073[0-6][0-9]{8}\|1844674407370[0-8][0-9]{6}\|18446744073709[0-4][0-9]{5}\|184467440737095[0-4][0-9]{4}\|18446744073709550[0-9]{3}\|18446744073709551[0-5][0-9]{2}\|1844674407370955160[0-9]{1}\|1844674407370955161[0-4]\|18446744073709551615)$` | Optional apply-time CAS version; omitted or null means unconditional. Overflow, leading zeros and signs are rejected. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Authoritative policy / 权威策略 |
| `400` | Invalid body, identity, flag or version / 无效请求 |
| `409` | Version conflict or exhaustion / 版本冲突或耗尽 |
| `503` | Authority unavailable or restore maintenance / 权威不可用或恢复维护 |
#### Response schema (`200`) — `UserSendBanResponse`
Policy response. Versions are independent for UID and source Channel.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | HTTP status repeated in the body. |
| `data` | `object` | yes | additionalProperties: `false` | Authoritative policy without credential fields. |
#### Response schema (`400`) — `SendBanError`
Policy error envelope; maintenance uses the separate MaintenanceError envelope.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | enum: `400`, `404`, `409`, `503` | HTTP failure status. |
| `code` | `string` | yes | enum: `"invalid_request"`, `"channel_not_found"`, `"version_conflict"`, `"channel_disbanded"`, `"version_exhausted"`, `"temporarily_unavailable"` | Typed policy failure; no current policy or version is included. |
| `msg` | `string` | yes | — | Error message; invalid input may use the generic invalid send ban request message. |
#### Response schema (`503`)
##### Referenced schema — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Acknowledge the latest command sync (/en/api/product-http/messages/ackCommandMessages)
* **Caller**: Trusted backend only
* **Scope**: Acknowledgement consumes only the generation recorded by the latest sync in this process; last\_message\_seq is only validated as positive and is not used for acknowledgement.
* **Success**: If no local generation exists, the endpoint still returns 200 and acknowledges nothing.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/message/syncack`
- Operation ID: `ackCommandMessages`
Requires a positive last_message_seq but acknowledges the server's latest recorded generation, not that supplied value.
#### Request schema — `MessageSyncAckRequest`
Legacy command-message synchronization acknowledgement parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | yes | minLength: 1; pattern: `\S` | Non-blank user ID acknowledging command-message synchronization. |
| `last_message_seq` | `integer:uint64` | yes | 1–∞ | Compatibility field that must be positive; acknowledgement uses the latest server-recorded generation rather than this supplied value. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Append a message event (/en/api/product-http/messages/appendMessageEvent)
* **Caller**: Trusted backend only
* **Scope**: Requires a committed persistent stream base with matching identity. Public events are delivered best effort to online Sessions; private/restricted events are not broadcast. Ordinary message sync does not enforce visibility access control; a trusted backend must authorize and filter.
* **Success**: Success confirms projection acceptance, not online display. Complete terminal snapshots and offline message sync provide recovery. Retry an uncertain write with the exact same event\_id and payload.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/message/event`
- Operation ID: `appendMessageEvent`
Validates and applies one message-scoped event projection; non-null headers are unsupported.
#### Request schema — `AppendMessageEventRequest`
Message-event projection parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | minLength: 1; pattern: `\S` | Target non-blank Channel ID; person and Agent Channels are normalized by the use case. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
| `from_uid` | `string` | no | — | Event actor UID; a non-empty value must match the committed base sender. Person or unencoded Agent Channel normalization may require it. |
| `message_id` | `integer:int64` | no | — | Optional server message ID; a positive value must match the committed base, while 0 or negative values are omitted from the event command. |
| `client_msg_no` | `string` | yes | minLength: 1; pattern: `\S` | Non-blank client message number of the message receiving the event. |
| `event_id` | `string` | yes | minLength: 1; pattern: `\S` | Non-blank unique identifier of this event. |
| `event_type` | `string` | yes | minLength: 1; pattern: `^\s*[sS][tT][rR][eE][aA][mM]\.(?:[oO][pP][eE][nN]\|[dD][eE][lL][tT][aA]\|[cC][lL][oO][sS][eE]\|[eE][rR][rR][oO][rR]\|[cC][aA][nN][cC][eE][lL]\|[sS][nN][aA][pP][sS][hH][oO][tT]\|[fF][iI][nN][iI][sS][hH])\s*$`; examples: `"stream.open"`, `"stream.delta"`, `"stream.close"`, `"stream.error"`, `"stream.cancel"`, `"stream.snapshot"`, `"stream.finish"` | Event type; surrounding whitespace is removed and the value is lowercased. |
| `event_key` | `string` | no | — | Event lane key; empty selects the default lane and stream.finish forces the finish lane. |
| `visibility` | `string` | no | — | Caller-supplied event metadata stored after normalization; empty defaults to public, and only public events are broadcast online. Ordinary message sync does not filter by it; eventsync filters by include_private without checking caller permissions. |
| `occurred_at` | `integer:int64` | no | — | Caller-supplied integer event time; the entry does not interpret its unit. |
| `payload` | `unknown` | no | — | Arbitrary JSON event payload passed to event storage as raw JSON bytes. |
| `headers` | `null` | no | — | Reserved field; it must be omitted or null, and every non-null JSON value is rejected. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Applied event projection |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `AppendMessageEventResponse`
Successful event-projection response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
| `data` | `AppendMessageEventData` | yes | additionalProperties: `false` | Normalized data for the applied event. |
##### Referenced schema — `AppendMessageEventData`
Normalized data for the applied event.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `client_msg_no` | `string` | yes | — | Client message number of the target message. |
| `event_key` | `string` | yes | — | Normalized event lane key. |
| `event_id` | `string` | yes | — | Accepted unique event identifier. |
| `msg_event_seq` | `integer:uint64` | yes | 0–∞ | Increasing sequence within this message event stream. |
| `stream_status` | `string` | yes | enum: `"open"`, `"closed"`, `"error"`, `"cancelled"` | Stream status after projection. |
| `channel_id` | `string` | yes | — | Normalized Channel ID. |
| `channel_type` | `integer:uint8` | yes | 0–255 | Normalized Channel type. |
| `from_uid` | `string` | yes | — | Normalized event actor UID. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Bind command-channel discovery (/en/api/product-http/messages/bindCommandChannel)
* **Caller**: Trusted backend only
* **Scope**: Persists discovery binding between a user and a command Channel; later /message/sync acknowledgement still depends on the current-process generation record.
* **Atomicity**: Choose uid plus source, uids plus source, or subscribers only. At most 1000 raw recipients and a 256 KiB body; subscribers must match SEND order.
* **Success**: Retry preserves live start and ack positions. Cross-Slot failure can be partial; retry the complete batch successfully before SEND.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/message/cmd/bind`
- Operation ID: `bindCommandChannel`
Starts bounded recipient discovery after one committed command tail.
#### Request schema — `MessageCMDBindingRequest`
Command-Channel offline-discovery binding parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | no | minLength: 1; pattern: `\S` | Unique identifier of the target user. |
| `channel_id` | `string` | no | minLength: 1; pattern: `\S` | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer:uint8` | no | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
| `uids` | array<`string`> | no | minItems: 1; maxItems: 1000 | Source-Channel recipients, at most 1000 entries; trims blanks and duplicates. Exclusive with uid and subscribers. |
| `subscribers` | array<`string`> | no | minItems: 1; maxItems: 1000 | Use this field alone to bind the SEND temporary recipient scope; at most 1000 entries, in the same order as SEND. Exclusive with source-Channel fields. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Messages (/en/api/product-http/messages)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Look up exact committed messages (/en/api/product-http/messages/lookupMessages)
* **Caller**: Trusted backend only
* **Scope**: Reads exact message indexes within current membership visibility, without a recent-history scan fallback.
* **Atomicity**: Content includes latest edits; temporary reads fail as 503 unavailable without partial messages.
* **Success**: Selectors form a union ordered and deduplicated by sequence; exhaustion or any failed read returns no partial result.
* **Recovery**: Every node must support the indexed-read RPC; older nodes reject it explicitly.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/messages`
- Operation ID: `lookupMessages`
Looks up exact committed messages within membership visibility; selector, result and scan limits fail without partial results.
#### Request schema — `MessageLookupRequest`
Exact message selectors within one visible channel. Selector arrays form a union; results are unique and ordered by sequence. The runtime validates the combined 1–128 selector count.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `login_uid` | `string` | yes | minLength: 1; pattern: `\S` | — |
| `channel_id` | `string` | yes | minLength: 1; pattern: `\S` | — |
| `channel_type` | `integer:uint8` | yes | 1–255 | — |
| `message_ids` | `array | null` | no | maxItems: 128 | Exact message IDs. Combined selector count must be 1–128. |
| `message_seqs` | `array | null` | no | maxItems: 128 | Exact message sequences. Combined selector count must be 1–128. |
| `client_msg_nos` | `array | null` | no | maxItems: 128 | Exact client message numbers. Combined selector count must be 1–128. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | One committed-message page |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Temporary read failure or restore maintenance; retry the same query with bounded backoff. |
#### Response schema (`200`) — `ChannelMessageSyncResponse`
Message synchronization page for one Channel.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `start_message_seq` | `integer:uint64` | yes | 0–∞ | Starting message sequence of the effective range for this page. |
| `end_message_seq` | `integer:uint64` | yes | 0–∞ | Ending message sequence of the effective range for this page. |
| `more` | `integer` | yes | enum: `0`, `1` | 1 means another page may exist in the same direction; 0 means the current range is exhausted. |
| `messages` | array<`LegacyMessage`> | yes | — | Messages are returned in ascending message_seq order regardless of pull direction. |
##### Referenced schema — `LegacyMessage`
Legacy message projection used by WuKongIM HTTP API synchronization.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `header` | `LegacyMessageHeader` | yes | additionalProperties: `false` | Legacy message header. |
| `setting` | `integer:uint8` | yes | 0–255 | WKProto Setting bitset. |
| `message_id` | `integer:int64` | yes | — | Server int64 message ID. |
| `message_idstr` | `string` | yes | — | Decimal string mirror of message_id only; it is not a replacement for message_seq. |
| `client_msg_no` | `string` | yes | — | Original client number, or the stable read-only alias wk3-legacy- for an old empty-number record with a message ID. The alias supports /messages lookup; it is not a SEND idempotency or event mutation key. Stored records are unchanged. |
| `end` | `integer:uint8` | no | 0–255 | Stream-message terminal compatibility flag. |
| `end_reason` | `integer:uint8` | no | 0–255 | Stream-message terminal Reason Code. |
| `error` | `string` | no | — | Stream-message error description. |
| `stream_data` | `string | null` | no | encoding: base64 | Base64-encoded stream data; it may be null. |
| `event_meta` | `LegacyMessageEventMeta` | no | additionalProperties: `false` | Optional message-event summary. |
| `event_sync_hint` | `LegacyMessageEventSyncHint` | no | additionalProperties: `false` | Optional message-event continuation hint. |
| `message_seq` | `integer:uint64` | yes | 0–∞ | uint64 sequence of the message within this Channel. |
| `from_uid` | `string` | yes | — | Message sender UID. |
| `channel_id` | `string` | yes | — | Channel ID containing the message. |
| `channel_type` | `integer:uint8` | yes | 0–255 | Channel type containing the message. |
| `topic` | `string` | no | — | Optional message topic. |
| `expire` | `integer:uint32` | yes | 0–∞ | Message expiration in seconds; 0 means unset. |
| `timestamp` | `integer:int32` | yes | — | Legacy server timestamp in seconds. |
| `payload` | `string | null` | yes | encoding: base64 | Base64-encoded message payload; it may be null. |
| `version` | `string` | no | pattern: `^[0-9]+$` | Edited content version; absent means zero in the same content epoch. |
| `updated_at_ms` | `integer:int64` | no | 0–∞ | Server edit time in Unix milliseconds; original ordering is unchanged. |
##### Referenced schema — `LegacyMessageHeader`
Legacy message header.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `no_persist` | `integer` | yes | enum: `0`, `1` | 1 means the message is non-durable. |
| `red_dot` | `integer` | yes | enum: `0`, `1` | 1 means the client should show a red-dot indicator. |
| `sync_once` | `integer` | yes | enum: `0`, `1` | 1 means a one-shot command message. |
##### Referenced schema — `LegacyMessageEventMeta`
Optional message-event summary.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `has_events` | `boolean` | yes | — | Whether events exist. |
| `completed` | `boolean` | yes | — | Whether all known event lanes have completed. |
| `event_version` | `integer:uint64` | no | 0–∞ | Event-summary version. |
| `last_msg_event_seq` | `integer:uint64` | no | 0–∞ | Latest event sequence across all lanes. |
| `event_count` | `integer` | no | 0–∞ | Total event count. |
| `open_event_count` | `integer` | no | 0–∞ | Number of event lanes still open. |
| `events` | array<`LegacyMessageEventKeyMeta`> | no | — | Per-lane event summaries. |
##### Referenced schema — `LegacyMessageEventKeyMeta`
Summary of one event lane.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `event_key` | `string` | yes | — | Event lane key. |
| `status` | `string` | yes | — | Current state of the lane. |
| `last_msg_event_seq` | `integer:uint64` | no | 0–∞ | Latest known event sequence for the lane. |
| `snapshot` | `unknown` | no | — | Arbitrary JSON snapshot in full summary mode. |
| `end_reason` | `integer:uint8` | no | 0–255 | Event-stream terminal Reason Code. |
| `error` | `string` | no | — | Event-stream error description. |
##### Referenced schema — `LegacyMessageEventSyncHint`
Optional message-event continuation hint.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `client_msg_no` | `string` | yes | — | Client message number of the target message. |
| `from_msg_event_seq` | `integer:uint64` | yes | 0–∞ | Starting sequence for the next event synchronization. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`)
##### Referenced schema — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
##### Referenced schema — `MessageUpdateError`
Edit or transient read failure; no partial results.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | — | HTTP status. |
| `code` | `string` | yes | — | Stable rejection code; uncertain writes retain their exact retry identity. |
| `msg` | `string` | no | — | Optional legacy diagnostic message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Synchronize up to 200 Channels (/en/api/product-http/messages/syncChannelMessagesBatch)
* **Caller**: Trusted backend only
* **Scope**: Each items entry runs independently; response order matches request order.
* **Success**: Even with HTTP 200, inspect each item error; one failed item does not make the whole batch non-2xx.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/messagesyncbatch`
- Operation ID: `syncChannelMessagesBatch`
Validates all memberships before one aligned batch read; item failures are embedded in the HTTP-200 response.
#### Request schema — `ChannelMessageSyncBatchRequest`
Batch Channel committed-message synchronization parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `login_uid` | `string` | yes | minLength: 1; pattern: `\S` | User identity performing the read and required to own the corresponding Channel membership. |
| `items` | array<`ChannelMessageSyncBatchItemRequest`> | yes | minItems: 1; maxItems: 200 | Channels to synchronize; between 1 and 200 items are accepted and response order matches request order. |
##### Referenced schema — `ChannelMessageSyncBatchItemRequest`
Read criteria for one Channel inside a batch synchronization request.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `login_uid` | `string` | no | — | Accepted for compatibility but ignored; batch requests always use the top-level login_uid. |
| `channel_id` | `string` | yes | minLength: 1; pattern: `\S` | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
| `start_message_seq` | `integer:uint64` | no | 0–∞; default: `0` | Inclusive Channel message-sequence starting boundary; 0 selects the open end, and newer reads also raise it to the membership visibility floor. |
| `end_message_seq` | `integer:uint64` | no | 0–∞; default: `0` | Exclusive Channel message-sequence ending boundary; 0 selects the open end. |
| `limit` | `integer` | no | default: `100` | Maximum messages returned by this read; values at or below 0 use 100 and values above 10000 are capped at 10000. |
| `pull_mode` | `integer:uint8` | no | 0–255; default: `0` | Pull direction; 1 reads toward newer messages and every other uint8 value uses the compatibility branch for older messages. |
| `include_event_meta` | `integer` | no | default: `0` | Message-event summary switch; a non-zero value requests event metadata and defaults an empty event_summary_mode to full. |
| `event_summary_mode` | `string` | no | default: `""` | Event-summary mode; empty disables enrichment, full includes snapshots, and any other non-empty value returns the basic summary without snapshots. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Aligned batch result |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Content epoch unavailable, restore transition, or restore maintenance; existing per-item batch errors are unchanged. |
#### Response schema (`200`) — `ChannelMessageSyncBatchResponse`
Batch result aligned in order with request items.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `items` | array<`ChannelMessageSyncBatchItemResponse`> | yes | — | Independent per-Channel synchronization results. |
##### Referenced schema — `ChannelMessageSyncBatchItemResponse`
Independent result for one Channel in a batch sync.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | — | Channel ID from the corresponding request item. |
| `channel_type` | `integer:uint8` | yes | 0–255 | Channel type from the corresponding request item. |
| `start_message_seq` | `integer:uint64` | yes | 0–∞ | Starting sequence of this item effective range. |
| `end_message_seq` | `integer:uint64` | yes | 0–∞ | Ending sequence of this item effective range. |
| `more` | `integer` | yes | enum: `0`, `1` | Whether another page may exist for this item. |
| `messages` | array<`LegacyMessage`> | yes | — | Messages returned when this item succeeds. |
| `error` | `string` | no | — | Per-item error text that must be checked even with HTTP 200; empty means no item error. |
##### Referenced schema — `LegacyMessage`
Legacy message projection used by WuKongIM HTTP API synchronization.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `header` | `LegacyMessageHeader` | yes | additionalProperties: `false` | Legacy message header. |
| `setting` | `integer:uint8` | yes | 0–255 | WKProto Setting bitset. |
| `message_id` | `integer:int64` | yes | — | Server int64 message ID. |
| `message_idstr` | `string` | yes | — | Decimal string mirror of message_id only; it is not a replacement for message_seq. |
| `client_msg_no` | `string` | yes | — | Original client number, or the stable read-only alias wk3-legacy- for an old empty-number record with a message ID. The alias supports /messages lookup; it is not a SEND idempotency or event mutation key. Stored records are unchanged. |
| `end` | `integer:uint8` | no | 0–255 | Stream-message terminal compatibility flag. |
| `end_reason` | `integer:uint8` | no | 0–255 | Stream-message terminal Reason Code. |
| `error` | `string` | no | — | Stream-message error description. |
| `stream_data` | `string | null` | no | encoding: base64 | Base64-encoded stream data; it may be null. |
| `event_meta` | `LegacyMessageEventMeta` | no | additionalProperties: `false` | Optional message-event summary. |
| `event_sync_hint` | `LegacyMessageEventSyncHint` | no | additionalProperties: `false` | Optional message-event continuation hint. |
| `message_seq` | `integer:uint64` | yes | 0–∞ | uint64 sequence of the message within this Channel. |
| `from_uid` | `string` | yes | — | Message sender UID. |
| `channel_id` | `string` | yes | — | Channel ID containing the message. |
| `channel_type` | `integer:uint8` | yes | 0–255 | Channel type containing the message. |
| `topic` | `string` | no | — | Optional message topic. |
| `expire` | `integer:uint32` | yes | 0–∞ | Message expiration in seconds; 0 means unset. |
| `timestamp` | `integer:int32` | yes | — | Legacy server timestamp in seconds. |
| `payload` | `string | null` | yes | encoding: base64 | Base64-encoded message payload; it may be null. |
| `version` | `string` | no | pattern: `^[0-9]+$` | Edited content version; absent means zero in the same content epoch. |
| `updated_at_ms` | `integer:int64` | no | 0–∞ | Server edit time in Unix milliseconds; original ordering is unchanged. |
##### Referenced schema — `LegacyMessageHeader`
Legacy message header.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `no_persist` | `integer` | yes | enum: `0`, `1` | 1 means the message is non-durable. |
| `red_dot` | `integer` | yes | enum: `0`, `1` | 1 means the client should show a red-dot indicator. |
| `sync_once` | `integer` | yes | enum: `0`, `1` | 1 means a one-shot command message. |
##### Referenced schema — `LegacyMessageEventMeta`
Optional message-event summary.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `has_events` | `boolean` | yes | — | Whether events exist. |
| `completed` | `boolean` | yes | — | Whether all known event lanes have completed. |
| `event_version` | `integer:uint64` | no | 0–∞ | Event-summary version. |
| `last_msg_event_seq` | `integer:uint64` | no | 0–∞ | Latest event sequence across all lanes. |
| `event_count` | `integer` | no | 0–∞ | Total event count. |
| `open_event_count` | `integer` | no | 0–∞ | Number of event lanes still open. |
| `events` | array<`LegacyMessageEventKeyMeta`> | no | — | Per-lane event summaries. |
##### Referenced schema — `LegacyMessageEventKeyMeta`
Summary of one event lane.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `event_key` | `string` | yes | — | Event lane key. |
| `status` | `string` | yes | — | Current state of the lane. |
| `last_msg_event_seq` | `integer:uint64` | no | 0–∞ | Latest known event sequence for the lane. |
| `snapshot` | `unknown` | no | — | Arbitrary JSON snapshot in full summary mode. |
| `end_reason` | `integer:uint8` | no | 0–255 | Event-stream terminal Reason Code. |
| `error` | `string` | no | — | Event-stream error description. |
##### Referenced schema — `LegacyMessageEventSyncHint`
Optional message-event continuation hint.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `client_msg_no` | `string` | yes | — | Client message number of the target message. |
| `from_msg_event_seq` | `integer:uint64` | yes | 0–∞ | Starting sequence for the next event synchronization. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`)
##### Referenced schema — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
##### Referenced schema — `MessageUpdateError`
Edit or transient read failure; no partial results.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | — | HTTP status. |
| `code` | `string` | yes | — | Stable rejection code; uncertain writes retain their exact retry identity. |
| `msg` | `string` | no | — | Optional legacy diagnostic message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Synchronize command messages (/en/api/product-http/messages/syncCommandMessages)
* **Caller**: Trusted backend only
* **Scope**: Messages come from durable storage, but the latest acknowledgement generation is kept only in the handling process memory, by default for about 5 minutes and at most 4,096 UIDs.
* **Success**: limit bounds returned messages, not the total cost of enumerating command Channels and scanning messages.
* **Recovery**: Keep sync and syncack node-affine; run sync again after process restart, expiry, or eviction.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/message/sync`
- Operation ID: `syncCommandMessages`
Returns the latest durable CMD generation; message_seq is accepted but ignored.
#### Request schema — `MessageSyncRequest`
Legacy command-message synchronization parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | yes | minLength: 1; pattern: `\S` | Non-blank user ID whose command messages are synchronized. |
| `message_seq` | `integer:uint64` | no | 0–∞ | Deprecated compatibility input accepted and ignored by the server. |
| `limit` | `integer` | no | 0–∞; default: `200` | Maximum messages returned; 0 uses 200, negative values are rejected, and values above 10000 are capped at 10000; it does not bound the total cost of enumerating Channels and scanning messages. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Bare legacy message array |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`)
##### Referenced schema — `LegacyMessage`
Legacy message projection used by WuKongIM HTTP API synchronization.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `header` | `LegacyMessageHeader` | yes | additionalProperties: `false` | Legacy message header. |
| `setting` | `integer:uint8` | yes | 0–255 | WKProto Setting bitset. |
| `message_id` | `integer:int64` | yes | — | Server int64 message ID. |
| `message_idstr` | `string` | yes | — | Decimal string mirror of message_id only; it is not a replacement for message_seq. |
| `client_msg_no` | `string` | yes | — | Original client number, or the stable read-only alias wk3-legacy- for an old empty-number record with a message ID. The alias supports /messages lookup; it is not a SEND idempotency or event mutation key. Stored records are unchanged. |
| `end` | `integer:uint8` | no | 0–255 | Stream-message terminal compatibility flag. |
| `end_reason` | `integer:uint8` | no | 0–255 | Stream-message terminal Reason Code. |
| `error` | `string` | no | — | Stream-message error description. |
| `stream_data` | `string | null` | no | encoding: base64 | Base64-encoded stream data; it may be null. |
| `event_meta` | `LegacyMessageEventMeta` | no | additionalProperties: `false` | Optional message-event summary. |
| `event_sync_hint` | `LegacyMessageEventSyncHint` | no | additionalProperties: `false` | Optional message-event continuation hint. |
| `message_seq` | `integer:uint64` | yes | 0–∞ | uint64 sequence of the message within this Channel. |
| `from_uid` | `string` | yes | — | Message sender UID. |
| `channel_id` | `string` | yes | — | Channel ID containing the message. |
| `channel_type` | `integer:uint8` | yes | 0–255 | Channel type containing the message. |
| `topic` | `string` | no | — | Optional message topic. |
| `expire` | `integer:uint32` | yes | 0–∞ | Message expiration in seconds; 0 means unset. |
| `timestamp` | `integer:int32` | yes | — | Legacy server timestamp in seconds. |
| `payload` | `string | null` | yes | encoding: base64 | Base64-encoded message payload; it may be null. |
| `version` | `string` | no | pattern: `^[0-9]+$` | Edited content version; absent means zero in the same content epoch. |
| `updated_at_ms` | `integer:int64` | no | 0–∞ | Server edit time in Unix milliseconds; original ordering is unchanged. |
##### Referenced schema — `LegacyMessageHeader`
Legacy message header.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `no_persist` | `integer` | yes | enum: `0`, `1` | 1 means the message is non-durable. |
| `red_dot` | `integer` | yes | enum: `0`, `1` | 1 means the client should show a red-dot indicator. |
| `sync_once` | `integer` | yes | enum: `0`, `1` | 1 means a one-shot command message. |
##### Referenced schema — `LegacyMessageEventMeta`
Optional message-event summary.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `has_events` | `boolean` | yes | — | Whether events exist. |
| `completed` | `boolean` | yes | — | Whether all known event lanes have completed. |
| `event_version` | `integer:uint64` | no | 0–∞ | Event-summary version. |
| `last_msg_event_seq` | `integer:uint64` | no | 0–∞ | Latest event sequence across all lanes. |
| `event_count` | `integer` | no | 0–∞ | Total event count. |
| `open_event_count` | `integer` | no | 0–∞ | Number of event lanes still open. |
| `events` | array<`LegacyMessageEventKeyMeta`> | no | — | Per-lane event summaries. |
##### Referenced schema — `LegacyMessageEventKeyMeta`
Summary of one event lane.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `event_key` | `string` | yes | — | Event lane key. |
| `status` | `string` | yes | — | Current state of the lane. |
| `last_msg_event_seq` | `integer:uint64` | no | 0–∞ | Latest known event sequence for the lane. |
| `snapshot` | `unknown` | no | — | Arbitrary JSON snapshot in full summary mode. |
| `end_reason` | `integer:uint8` | no | 0–255 | Event-stream terminal Reason Code. |
| `error` | `string` | no | — | Event-stream error description. |
##### Referenced schema — `LegacyMessageEventSyncHint`
Optional message-event continuation hint.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `client_msg_no` | `string` | yes | — | Client message number of the target message. |
| `from_msg_event_seq` | `integer:uint64` | yes | 0–∞ | Starting sequence for the next event synchronization. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Synchronize current message event projections (/en/api/product-http/messages/syncMessageEvents)
* **Caller**: Trusted backend only
* **Scope**: Reads current durable lane projections, not complete event history. This entry checks neither caller identity nor membership; include\_private is caller-controlled and requires a trusted backend.
* **Success**: Reads limit+1 raw projections before filtering private/restricted entries. A filtered empty page or more=0 does not prove there are no visible projections farther ahead. next\_msg\_event\_seq advances only for returned items.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/message/eventsync`
- Operation ID: `syncMessageEvents`
Reads current durable event projections after a sequence cursor, with filtering after a bounded read; not a replayable event log.
#### Request schema — `MessageEventSyncRequest`
Read parameters for current durable message event projections; membership is not checked.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `channel_id` | `string` | yes | pattern: `\S` | Non-blank Channel ID; a person channel with non-blank from_uid is normalized using both UIDs. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Channel type in the range 1–255. |
| `from_uid` | `string` | no | — | Optional sender UID used only for person-channel normalization, not an authenticated identity. |
| `client_msg_no` | `string` | yes | pattern: `\S` | Non-blank client message number of the parent message. |
| `event_key` | `string` | no | — | Optional event-lane filter; empty reads all lanes. |
| `from_msg_event_seq` | `integer:uint64` | no | 0–∞; default: `0` | Exclusive sequence boundary; read only current projections after this value, default 0. |
| `limit` | `integer` | no | 0–∞; default: `0` | Omit or use 0 for 200; negative values are rejected and values above 2000 are capped at 2000. |
| `include_private` | `integer:uint8` | no | 0–255; default: `0` | Exactly 1 includes private/restricted projections; other uint8 values filter them. This switch is not an authorization check. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Current durable event projections |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `MessageEventSyncResponse`
Successful message-event read response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application status fixed at 200. |
| `data` | `MessageEventSyncData` | yes | additionalProperties: `false` | Current projections and cursors. |
##### Referenced schema — `MessageEventSyncData`
Current projections and cursors.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `client_msg_no` | `string` | yes | — | Requested client message number. |
| `from_msg_event_seq` | `integer:uint64` | yes | 0–∞ | Echoed request starting cursor. |
| `next_msg_event_seq` | `integer:uint64` | yes | 0–∞ | Sequence of the last returned projection; unchanged from the input cursor when no items are returned. |
| `more` | `integer` | yes | enum: `0`, `1` | 1 when another returnable item exists in the bounded raw page; filtering can make it 0 even when visible projections exist farther ahead. |
| `events` | array<`SyncedMessageEvent`> | yes | maxItems: 2000 | Current projections ordered by event sequence, at most 2000 items. |
| `filtered_by_event_key` | `string` | yes | — | Echoed event_key; empty means no lane filter. |
##### Referenced schema — `SyncedMessageEvent`
Current projection of one event lane, not every historical event.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg_event_seq` | `integer:uint64` | yes | 0–∞ | Latest event sequence of the projection. |
| `event_id` | `string` | yes | — | Latest event ID. |
| `event_key` | `string` | yes | — | Event lane key. |
| `event_type` | `string` | yes | — | Latest event type; a blank original value returns stream.snapshot. |
| `visibility` | `string` | yes | — | Visibility string stored with the projection. |
| `occurred_at` | `integer:int64` | yes | — | Original event time value stored with the projection. |
| `payload` | `unknown` | no | — | Snapshot JSON value; non-JSON bytes become a string, and a missing snapshot uses an object with status/end_reason and optional error. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Pull edits for one channel (/en/api/product-http/messages/syncMessageUpdates)
* **Caller**: Trusted backend only
* **Scope**: Synchronize the currently viewed channel on entry, reconnect or resume; this endpoint does not discover updates for all conversations.
* **Success**: Obtain a baseline before loading visible history; continue while more=true even when updates is empty. Merge messages and cursor transactionally; an older message edit must not replace a newer tail preview.
* **Recovery**: On reset\_required=true, reload visible history from the new baseline; on 503, preserve the cursor and retry with backoff. Discard delayed responses from older content epochs.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/messageupdates`
- Operation ID: `syncMessageUpdates`
Bootstrap with an empty cursor before loading history. Drain more pages and merge versions with the cursor atomically; this feed returns latest states.
#### Request schema — `MessageUpdatesRequest`
Pull latest edits for one visible channel.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `login_uid` | `string` | yes | — | User identity; required for person-channel normalization and edit sync. It does not authorize editing. |
| `channel_id` | `string` | yes | minLength: 1 | Channel ID; use the peer UID for a person channel. |
| `channel_type` | `integer` | yes | 1–255 | Channel type: 1 person, 2 group. |
| `update_cursor` | `string` | no | maxLength: 4096 | Opaque cursor, at most 4096 bytes. Empty bootstraps before loading history. |
| `limit` | `integer` | no | 0–200; default: `100` | Maximum updates; omitted or 0 uses 100. |
#### Request examples
##### default
```json
{
"login_uid": "bob",
"channel_id": "alice",
"channel_type": 1,
"update_cursor": "",
"limit": 100
}
```
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Success |
| `400` | invalid_request |
| `403` | channel_not_accessible |
| `404` | message_not_found |
| `409` | version_conflict, content_epoch_conflict, idempotency_conflict, reset_required or stale_meta |
| `422` | message_not_updatable |
| `429` | resource_exhausted |
| `503` | Temporary dependency failure or restore maintenance |
#### Response schema (`200`) — `MessageUpdatesResponse`
Latest-state incremental page.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `updates` | array<`UpdatedMessage`> | yes | maxItems: 200 | Latest states, not a full edit history; empty is possible while more is true. |
| `next_update_cursor` | `string` | yes | — | Commit with merged rows in one local transaction. |
| `more` | `boolean` | yes | — | Continue this channel until false. |
| `reset_required` | `boolean` | yes | — | Hold the returned baseline, reload visible history, then commit history and baseline in one local transaction. Reset old version comparisons after an epoch change. |
##### Referenced schema — `UpdatedMessage`
Legacy message projection used by WuKongIM HTTP API synchronization.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `header` | `LegacyMessageHeader` | yes | additionalProperties: `false` | Legacy message header. |
| `setting` | `integer:uint8` | yes | 0–255 | WKProto Setting bitset. |
| `message_id` | `string` | yes | pattern: `^[0-9]+$` | Server int64 message ID. |
| `message_idstr` | `string` | yes | — | Decimal string mirror of message_id only; it is not a replacement for message_seq. |
| `client_msg_no` | `string` | yes | — | Original client number, or the stable read-only alias wk3-legacy- for an old empty-number record with a message ID. The alias supports /messages lookup; it is not a SEND idempotency or event mutation key. Stored records are unchanged. |
| `end` | `integer:uint8` | no | 0–255 | Stream-message terminal compatibility flag. |
| `end_reason` | `integer:uint8` | no | 0–255 | Stream-message terminal Reason Code. |
| `error` | `string` | no | — | Stream-message error description. |
| `stream_data` | `string | null` | no | encoding: base64 | Base64-encoded stream data; it may be null. |
| `event_meta` | `LegacyMessageEventMeta` | no | additionalProperties: `false` | Optional message-event summary. |
| `event_sync_hint` | `LegacyMessageEventSyncHint` | no | additionalProperties: `false` | Optional message-event continuation hint. |
| `message_seq` | `string` | yes | pattern: `^[0-9]+$` | uint64 sequence of the message within this Channel. |
| `from_uid` | `string` | yes | — | Message sender UID. |
| `channel_id` | `string` | yes | — | Channel ID containing the message. |
| `channel_type` | `integer:uint8` | yes | 0–255 | Channel type containing the message. |
| `topic` | `string` | no | — | Optional message topic. |
| `expire` | `integer:uint32` | yes | 0–∞ | Message expiration in seconds; 0 means unset. |
| `timestamp` | `integer:int32` | yes | — | Legacy server timestamp in seconds. |
| `payload` | `string | null` | yes | encoding: base64 | Base64-encoded message payload; it may be null. |
| `version` | `string` | yes | pattern: `^[0-9]+$` | Edited content version; absent means zero in the same content epoch. |
| `updated_at_ms` | `integer:int64` | yes | 0–∞ | Server edit time in Unix milliseconds; original ordering is unchanged. |
##### Referenced schema — `LegacyMessageHeader`
Legacy message header.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `no_persist` | `integer` | yes | enum: `0`, `1` | 1 means the message is non-durable. |
| `red_dot` | `integer` | yes | enum: `0`, `1` | 1 means the client should show a red-dot indicator. |
| `sync_once` | `integer` | yes | enum: `0`, `1` | 1 means a one-shot command message. |
##### Referenced schema — `LegacyMessageEventMeta`
Optional message-event summary.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `has_events` | `boolean` | yes | — | Whether events exist. |
| `completed` | `boolean` | yes | — | Whether all known event lanes have completed. |
| `event_version` | `integer:uint64` | no | 0–∞ | Event-summary version. |
| `last_msg_event_seq` | `integer:uint64` | no | 0–∞ | Latest event sequence across all lanes. |
| `event_count` | `integer` | no | 0–∞ | Total event count. |
| `open_event_count` | `integer` | no | 0–∞ | Number of event lanes still open. |
| `events` | array<`LegacyMessageEventKeyMeta`> | no | — | Per-lane event summaries. |
##### Referenced schema — `LegacyMessageEventKeyMeta`
Summary of one event lane.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `event_key` | `string` | yes | — | Event lane key. |
| `status` | `string` | yes | — | Current state of the lane. |
| `last_msg_event_seq` | `integer:uint64` | no | 0–∞ | Latest known event sequence for the lane. |
| `snapshot` | `unknown` | no | — | Arbitrary JSON snapshot in full summary mode. |
| `end_reason` | `integer:uint8` | no | 0–255 | Event-stream terminal Reason Code. |
| `error` | `string` | no | — | Event-stream error description. |
##### Referenced schema — `LegacyMessageEventSyncHint`
Optional message-event continuation hint.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `client_msg_no` | `string` | yes | — | Client message number of the target message. |
| `from_msg_event_seq` | `integer:uint64` | yes | 0–∞ | Starting sequence for the next event synchronization. |
#### Response schema (`400`) — `MessageUpdateError`
Edit or transient read failure; no partial results.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | — | HTTP status. |
| `code` | `string` | yes | — | Stable rejection code; uncertain writes retain their exact retry identity. |
| `msg` | `string` | no | — | Optional legacy diagnostic message. |
#### Response schema (`503`)
##### Referenced schema — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Synchronize one Channel's committed messages (/en/api/product-http/messages/syncQuickstartChannelMessages)
* **Caller**: Trusted backend only
* **Scope**: The login\_uid must have an ordinary membership before reading; join\_seq and deleted\_to\_seq establish the lowest visible sequence. Returned messages are always ordered by ascending message\_seq.
* **Success**: pull\_mode=1 reads \[start\_message\_seq, end\_message\_seq); other values read older messages in (end\_message\_seq, start\_message\_seq]. A zero boundary selects the open end.
* **Recovery**: Page reads include latest edits; retry the original query on 503 unavailable. Reset cached version comparisons after a content epoch increase.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/channel/messagesync`
- Operation ID: `syncQuickstartChannelMessages`
Checks membership visibility and returns an ascending page; limit is 100 by default and capped at 10000.
#### Trusted-backend code samples
##### Trusted backend (cURL)
```bash
# Run only from the trusted backend boundary.
curl --fail-with-body \
--request POST \
--url http://127.0.0.1:5001/channel/messagesync \
--header 'content-type: application/json' \
--data '{"login_uid":"bob","channel_id":"alice","channel_type":1,"start_message_seq":0,"end_message_seq":0,"limit":100,"pull_mode":1,"include_event_meta":0,"event_summary_mode":""}'
```
#### Request schema — `ChannelMessageSyncRequest`
Single-Channel committed-message synchronization parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `login_uid` | `string` | yes | minLength: 1; pattern: `\S` | User identity performing the read and required to own the corresponding Channel membership. |
| `channel_id` | `string` | yes | minLength: 1; pattern: `\S` | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer:uint8` | yes | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
| `start_message_seq` | `integer:uint64` | no | 0–∞; default: `0` | Inclusive Channel message-sequence starting boundary; 0 selects the open end, and newer reads also raise it to the membership visibility floor. |
| `end_message_seq` | `integer:uint64` | no | 0–∞; default: `0` | Exclusive Channel message-sequence ending boundary; 0 selects the open end. |
| `limit` | `integer` | no | default: `100` | Maximum messages returned by this read; values at or below 0 use 100 and values above 10000 are capped at 10000. |
| `pull_mode` | `integer:uint8` | no | 0–255; default: `0` | Pull direction; 1 reads toward newer messages and every other uint8 value uses the compatibility branch for older messages. |
| `include_event_meta` | `integer` | no | default: `0` | Message-event summary switch; a non-zero value requests event metadata and defaults an empty event_summary_mode to full. |
| `event_summary_mode` | `string` | no | default: `""` | Event-summary mode; empty disables enrichment, full includes snapshots, and any other non-empty value returns the basic summary without snapshots. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | One committed-message page |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Temporary read failure or restore maintenance; retry the same query with bounded backoff. |
#### Response schema (`200`) — `ChannelMessageSyncResponse`
Message synchronization page for one Channel.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `start_message_seq` | `integer:uint64` | yes | 0–∞ | Starting message sequence of the effective range for this page. |
| `end_message_seq` | `integer:uint64` | yes | 0–∞ | Ending message sequence of the effective range for this page. |
| `more` | `integer` | yes | enum: `0`, `1` | 1 means another page may exist in the same direction; 0 means the current range is exhausted. |
| `messages` | array<`LegacyMessage`> | yes | — | Messages are returned in ascending message_seq order regardless of pull direction. |
##### Referenced schema — `LegacyMessage`
Legacy message projection used by WuKongIM HTTP API synchronization.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `header` | `LegacyMessageHeader` | yes | additionalProperties: `false` | Legacy message header. |
| `setting` | `integer:uint8` | yes | 0–255 | WKProto Setting bitset. |
| `message_id` | `integer:int64` | yes | — | Server int64 message ID. |
| `message_idstr` | `string` | yes | — | Decimal string mirror of message_id only; it is not a replacement for message_seq. |
| `client_msg_no` | `string` | yes | — | Original client number, or the stable read-only alias wk3-legacy- for an old empty-number record with a message ID. The alias supports /messages lookup; it is not a SEND idempotency or event mutation key. Stored records are unchanged. |
| `end` | `integer:uint8` | no | 0–255 | Stream-message terminal compatibility flag. |
| `end_reason` | `integer:uint8` | no | 0–255 | Stream-message terminal Reason Code. |
| `error` | `string` | no | — | Stream-message error description. |
| `stream_data` | `string | null` | no | encoding: base64 | Base64-encoded stream data; it may be null. |
| `event_meta` | `LegacyMessageEventMeta` | no | additionalProperties: `false` | Optional message-event summary. |
| `event_sync_hint` | `LegacyMessageEventSyncHint` | no | additionalProperties: `false` | Optional message-event continuation hint. |
| `message_seq` | `integer:uint64` | yes | 0–∞ | uint64 sequence of the message within this Channel. |
| `from_uid` | `string` | yes | — | Message sender UID. |
| `channel_id` | `string` | yes | — | Channel ID containing the message. |
| `channel_type` | `integer:uint8` | yes | 0–255 | Channel type containing the message. |
| `topic` | `string` | no | — | Optional message topic. |
| `expire` | `integer:uint32` | yes | 0–∞ | Message expiration in seconds; 0 means unset. |
| `timestamp` | `integer:int32` | yes | — | Legacy server timestamp in seconds. |
| `payload` | `string | null` | yes | encoding: base64 | Base64-encoded message payload; it may be null. |
| `version` | `string` | no | pattern: `^[0-9]+$` | Edited content version; absent means zero in the same content epoch. |
| `updated_at_ms` | `integer:int64` | no | 0–∞ | Server edit time in Unix milliseconds; original ordering is unchanged. |
##### Referenced schema — `LegacyMessageHeader`
Legacy message header.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `no_persist` | `integer` | yes | enum: `0`, `1` | 1 means the message is non-durable. |
| `red_dot` | `integer` | yes | enum: `0`, `1` | 1 means the client should show a red-dot indicator. |
| `sync_once` | `integer` | yes | enum: `0`, `1` | 1 means a one-shot command message. |
##### Referenced schema — `LegacyMessageEventMeta`
Optional message-event summary.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `has_events` | `boolean` | yes | — | Whether events exist. |
| `completed` | `boolean` | yes | — | Whether all known event lanes have completed. |
| `event_version` | `integer:uint64` | no | 0–∞ | Event-summary version. |
| `last_msg_event_seq` | `integer:uint64` | no | 0–∞ | Latest event sequence across all lanes. |
| `event_count` | `integer` | no | 0–∞ | Total event count. |
| `open_event_count` | `integer` | no | 0–∞ | Number of event lanes still open. |
| `events` | array<`LegacyMessageEventKeyMeta`> | no | — | Per-lane event summaries. |
##### Referenced schema — `LegacyMessageEventKeyMeta`
Summary of one event lane.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `event_key` | `string` | yes | — | Event lane key. |
| `status` | `string` | yes | — | Current state of the lane. |
| `last_msg_event_seq` | `integer:uint64` | no | 0–∞ | Latest known event sequence for the lane. |
| `snapshot` | `unknown` | no | — | Arbitrary JSON snapshot in full summary mode. |
| `end_reason` | `integer:uint8` | no | 0–255 | Event-stream terminal Reason Code. |
| `error` | `string` | no | — | Event-stream error description. |
##### Referenced schema — `LegacyMessageEventSyncHint`
Optional message-event continuation hint.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `client_msg_no` | `string` | yes | — | Client message number of the target message. |
| `from_msg_event_seq` | `integer:uint64` | yes | 0–∞ | Starting sequence for the next event synchronization. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`)
##### Referenced schema — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
##### Referenced schema — `MessageUpdateError`
Edit or transient read failure; no partial results.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | — | HTTP status. |
| `code` | `string` | yes | — | Stable rejection code; uncertain writes retain their exact retry identity. |
| `msg` | `string` | no | — | Optional legacy diagnostic message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Unbind command-channel discovery (/en/api/product-http/messages/unbindCommandChannel)
* **Caller**: Trusted backend only
* **Scope**: Uses the same three exclusive forms and 1000-entry/256 KiB bounds as bind; temporary scope order must match SEND.
* **Success**: Cross-Slot failure can be partial; retry the complete batch. Unbinding does not delete messages.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/message/cmd/unbind`
- Operation ID: `unbindCommandChannel`
Tombstones bounded recipient discovery without deleting command messages.
#### Request schema — `MessageCMDBindingRequest`
Command-Channel offline-discovery binding parameters.
- Additional properties: `true`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `uid` | `string` | no | minLength: 1; pattern: `\S` | Unique identifier of the target user. |
| `channel_id` | `string` | no | minLength: 1; pattern: `\S` | Logical identifier of the target Channel; for a person Channel this is normally the other UID. |
| `channel_type` | `integer:uint8` | no | 1–255 | Numeric Channel type; 1 is a person Channel, 2 is a group Channel, and other values are extension types. |
| `uids` | array<`string`> | no | minItems: 1; maxItems: 1000 | Source-Channel recipients, at most 1000 entries; trims blanks and duplicates. Exclusive with uid and subscribers. |
| `subscribers` | array<`string`> | no | minItems: 1; maxItems: 1000 | Use this field alone to bind the SEND temporary recipient scope; at most 1000 entries, in the same order as SEND. Exclusive with source-Channel fields. |
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Mutation accepted |
| `400` | Input, validation, dependency, or downstream error |
| `503` | Controller restore maintenance is active |
#### Response schema (`200`) — `StatusEnvelope`
Legacy mutation-success response.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | Application-level status fixed at 200. |
#### Response schema (`400`) — `CompatibilityError`
HTTP 400 error body shared by multiple legacy entries.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `msg` | `string` | yes | — | Human-readable error text with no stability guarantee; do not branch on it. |
| `status` | `integer` | yes | const: `400` | Application-level status fixed at 400. |
#### Response schema (`503`) — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Replace a message payload (/en/api/product-http/messages/updateMessage)
* **Caller**: Trusted backend only
* **Scope**: The backend owns editing permissions and time windows; message ID, sequence, original timestamp, unread and ordering stay unchanged.
* **Success**: 200 means the edit and idempotency result were quorum committed and applied by the Slot; it does not wait for devices.
* **Recovery**: Retry uncertain outcomes with identical request\_id, version, content epoch and payload. After a version or epoch conflict, reload and make a new editing decision.
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
## OpenAPI 3.1 operation contract
This machine-readable summary and the Fumadocs reference on the page come from the complete 48-operation runtime contract. WuKongIM HTTP API has no built-in authentication and is callable only from a trusted backend or operator boundary.
### `POST` `/message/update`
- Operation ID: `updateMessage`
Replaces an ordinary retained payload with version and restore-epoch checks. CMD, stream and nonpersistent messages cannot be edited.
#### Request schema — `UpdateMessageRequest`
Replace an ordinary retained message; the backend owns editing permission.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `login_uid` | `string` | no | — | User identity; required for person-channel normalization and edit sync. It does not authorize editing. |
| `channel_id` | `string` | yes | minLength: 1 | Channel ID; use the peer UID for a person channel. |
| `channel_type` | `integer` | yes | 1–255 | Channel type: 1 person, 2 group. |
| `message_id` | `string` | yes | pattern: `^[0-9]+$` | Existing nonzero message ID. |
| `expected_content_epoch` | `string` | yes | pattern: `^[0-9]+$` | Epoch from the response that supplied the displayed message. |
| `expected_version` | `string` | yes | pattern: `^[0-9]+$` | Displayed version; use 0 for an unedited original. |
| `request_id` | `string` | yes | minLength: 1; maxLength: 128 | Stable retry ID, at most 128 UTF-8 bytes. |
| `payload` | `string` | yes | minLength: 1; encoding: base64 | Complete replacement; 1 to 1 MiB decoded bytes, 2 MiB request-body limit. |
#### Request examples
##### default
```json
{
"login_uid": "alice",
"channel_id": "bob",
"channel_type": 1,
"message_id": "12345",
"expected_version": "0",
"expected_content_epoch": "0",
"request_id": "5893e137-1994-4377-b4ed-1f0c9e8fe4bd",
"payload": "bmV3"
}
```
#### Response statuses
| Status | Description |
| --- | --- |
| `200` | Success |
| `400` | invalid_request |
| `403` | channel_not_accessible |
| `404` | message_not_found |
| `409` | version_conflict, content_epoch_conflict, idempotency_conflict, reset_required or stale_meta |
| `422` | message_not_updatable |
| `429` | resource_exhausted |
| `503` | Temporary dependency failure or restore maintenance |
#### Response schema (`200`) — `UpdateMessageResponse`
Successful edit envelope.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | const: `200` | HTTP status. |
| `data` | `MessageUpdateResult` | yes | additionalProperties: `false` | Committed edit result. |
##### Referenced schema — `MessageUpdateResult`
Committed edit result.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `message_id` | `string` | yes | pattern: `^[0-9]+$` | Original message ID. |
| `message_seq` | `string` | yes | pattern: `^[0-9]+$` | Original channel sequence. |
| `version` | `string` | yes | pattern: `^[0-9]+$` | Edited content version; absent means zero in the same content epoch. |
| `updated_at_ms` | `integer:int64` | yes | 0–∞ | Server edit time in Unix milliseconds; original ordering is unchanged. |
#### Response schema (`400`) — `MessageUpdateError`
Edit or transient read failure; no partial results.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `status` | `integer` | yes | — | HTTP status. |
| `code` | `string` | yes | — | Stable rejection code; uncertain writes retain their exact retry identity. |
| `msg` | `string` | no | — | Optional legacy diagnostic message. |
#### Response schema (`503`)
##### Referenced schema — `MaintenanceError`
503 error body returned while cluster restore maintenance is active.
- Additional properties: `false`
| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `error` | `string` | yes | const: `"maintenance"` | Stable maintenance error category. |
| `message` | `string` | yes | const: `"restore maintenance is active"` | Current restore-maintenance message. |
[OpenAPI 3.1 complete runtime contract](/contracts/product-http.openapi.json)
# Android Custom Messages (/en/sdk/android/advanced/custom-messages)
Content beyond text and images, such as an order card or shared location, can be a `WKMessageContent` subclass. Every client must use the same type number and JSON fields for the same content.
## 1. Define the content [#1-define-the-content]
`10001` is only an example; maintain a shared registry of type numbers so product messages do not collide.
```java
public final class OrderMessageContent extends WKMessageContent {
public String orderID = "";
public String title = "";
// The SDK decodes through this no-argument constructor. Do not remove it.
public OrderMessageContent() {
type = 10001;
}
public OrderMessageContent(String orderID, String title) {
this();
this.orderID = orderID;
this.title = title;
}
@Override
public JSONObject encodeMsg() {
JSONObject json = new JSONObject();
try {
json.put("order_id", orderID);
json.put("title", title);
} catch (JSONException ignored) {
}
return json;
}
@Override
public WKMessageContent decodeMsg(JSONObject json) {
orderID = json.optString("order_id", "");
title = json.optString("title", "");
return this;
}
@Override public String getDisplayContent() {
return "[Order] " + title;
}
@Override public String getSearchableWord() {
return title;
}
}
```
If this content crosses Android component boundaries through `Parcel`, also implement the subclass parcel constructor, `writeToParcel`, and `CREATOR`, following the built-in SDK content types.
## 2. Register after initialization [#2-register-after-initialization]
```java
WKIM.getInstance().init(applicationContext, uid, token);
WKIM.getInstance().getMsgManager()
.registerContentMsg(OrderMessageContent.class);
```
Register before connecting or reading messages. The class must have a public no-argument constructor so the SDK can decode it.
## 3. Send and render [#3-send-and-render]
```java
OrderMessageContent content =
new OrderMessageContent("order-2026-001", "Coffee beans");
WKChannel channel = new WKChannel("bob", WKChannelType.PERSONAL);
WKIM.getInstance().getMsgManager().send(content, channel);
```
Inspect the runtime type when receiving, and show a generic placeholder for unknown content:
```java
if (message.baseContentMsgModel instanceof OrderMessageContent) {
OrderMessageContent order =
(OrderMessageContent) message.baseContentMsgModel;
showOrder(order.orderID, order.title);
} else {
showUnsupportedMessage();
}
```
Give new fields defaults, do not reuse old fields for new meanings, and never change a published type number.
# Android Advanced (/en/sdk/android/advanced)
Complete the [Quickstart](/en/sdk/android/quickstart), then add only the advanced capabilities your product needs. Unused providers do not need configuration.
## Before production [#before-production]
* `1.5.5` uses raw TCP by default, with no authenticated TLS switch in the public API. Use a controlled network entry or a transport design your security team has approved.
* Keep `setDebug(false)` and confirm Release logs do not expose tokens or message bodies.
* The token is stored in ordinary app preferences, and the local database does not replace system credential protection. Use an Android Keystore-backed design for sensitive credentials, with explicit backup and sign-out cleanup rules.
* The app owns shrinking rules. Test connection, messages, and custom content in an R8-enabled Release build.
* Test poor networks, background recovery, expired tokens, kicks, offline messages, account switching, and rollback on real devices.
# Android Media & History (/en/sdk/android/advanced/media-and-history)
A media message must upload its local file to a remote location available to other devices. The SDK owns message state; your app performs object-storage upload and authorization.
## Send an image [#send-an-image]
```java
WKImageContent image = new WKImageContent(localPath);
image.width = width;
image.height = height;
WKChannel channel = new WKChannel("team-1", WKChannelType.GROUP);
WKIM.getInstance().getMsgManager().send(image, channel);
```
Configure attachment upload before sending media:
```java
msgManager.addOnUploadAttachListener((message, result) -> {
if (!(message.baseContentMsgModel instanceof WKImageContent)) {
result.onUploadResult(false, message.baseContentMsgModel);
return;
}
WKImageContent content =
(WKImageContent) message.baseContentMsgModel;
objectStorage.upload(content.localPath, (url, error) -> {
if (error == null) {
content.url = url;
result.onUploadResult(true, content);
} else {
result.onUploadResult(false, content);
}
});
});
```
Upload credentials should be short lived and limited by path, size, and file type. Never treat `localPath` as a URL the receiving device can access.
## Connect remote history [#connect-remote-history]
`getOrSyncHistoryMessages` reads the local database first. It invokes the channel synchronization listener when the local range is incomplete:
```java
msgManager.addOnSyncChannelMsgListener((
channelID,
channelType,
startMessageSeq,
endMessageSeq,
limit,
pullMode,
callback
) -> historyApi.fetch(
channelID,
channelType,
startMessageSeq,
endMessageSeq,
limit,
pullMode,
callback::onBack
));
```
The application API converts its response to `WKSyncChannelMsg`, with a `WKSyncRecent` list in `messages`. Complete with an empty result when there is no data; never leave the screen waiting forever.
## Common problems [#common-problems]
* Image remains sending: ensure upload always calls `onUploadResult` and writes `content.url` on success.
* Other devices cannot display the image: make sure the receiver can authorize and download the URL; it cannot use the sender's sandbox path.
* Duplicate history: merge by channel and `messageSeq`, not array position.
* History never finishes: time out the application request and end UI loading on success, empty data, and failure.
# Android quickstart (/en/sdk/easy/android/getting-started)
This tutorial uses **1.0.5** to exchange online messages between Alice and Bob in separate clients. WebSocket JSON-RPC CONNECT authenticates the connection.
## 1. Prepare [#1-prepare]
Prepare the following:
* Android 5.0 (API 21) or later.
* An AndroidX application. For an exact reference, this tag itself uses Kotlin `1.9.0`, Android Gradle Plugin `8.1.4`, Gradle `8.4`, and compileSdk `34`.
* A WuKongIM single-node cluster or multi-node cluster whose `/readyz` is healthy and whose WebSocket Gateway is reachable from the Android device.
* A product backend that returns `uid`, a short-lived `token`, and `websocketUrl` separately for Alice and Bob.
Read [Identity & Token](/en/guide/integration/authentication) first. Example variables come from the product backend; real tokens never belong in the APK, repository, Logcat, or crash reports.
To see messages first, follow [the official example](/en/sdk/easy/examples#android) with two clients. The steps below integrate the SDK into your application.
The default device category is **APP `0`**. Store the Token with the same `device_flag` on your backend: APP is `0`, WEB is `1`, and PC is `2`.
## 2. Install the SDK [#2-install-the-sdk]
Add this to the application module's `build.gradle.kts`:
```kotlin
dependencies {
implementation("com.githubim:easysdk-android:1.0.5")
}
```
The equivalent Groovy DSL is:
```groovy
dependencies {
implementation 'com.githubim:easysdk-android:1.0.5'
}
```
Confirm that `gradle.properties` enables AndroidX:
```properties
android.useAndroidX=true
```
### Network permissions [#network-permissions]
Add these entries to `AndroidManifest.xml`:
```xml
```
Production uses `wss://`. Android 9 and later restrict cleartext traffic by default; do not globally enable cleartext as a production TLS workaround.
## 3. Connect and listen [#3-connect-and-listen]
```kotlin
data class IMBootstrap(
val uid: String,
val token: String,
val websocketUrl: String,
)
```
The product backend authenticates the current product user and selects a WebSocket address. The Android client receives only its own connection material and no WuKongIM HTTP API management credential.
Android EasySDK is a process singleton, and `v1.0.5` cannot be initialized a second time with another configuration. This example fits a minimal run with one UID per process. A production application that switches accounts must first design and test a complete logout strategy around this limitation.
```kotlin
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.githubim.easysdk.WuKongConfig
import com.githubim.easysdk.WuKongEasySDK
import com.githubim.easysdk.enums.WuKongChannelType
import com.githubim.easysdk.enums.WuKongEvent
import com.githubim.easysdk.listener.WuKongEventListener
import com.githubim.easysdk.model.ConnectResult
import com.githubim.easysdk.model.DisconnectInfo
import com.githubim.easysdk.model.Message
import com.githubim.easysdk.model.WuKongError
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
class ChatActivity : AppCompatActivity() {
private val easySDK = WuKongEasySDK.getInstance()
private var listenersRegistered = false
private var connected = false
private val connectListener = object : WuKongEventListener {
override fun onEvent(result: ConnectResult) = runOnUiThread {
connected = true
Log.i("EasySDK", "connected")
}
}
private val messageListener = object : WuKongEventListener {
override fun onEvent(message: Message) = runOnUiThread {
Log.i("EasySDK", "message received")
// Deduplicate by messageId before updating the UI.
}
}
private val disconnectListener = object : WuKongEventListener {
override fun onEvent(info: DisconnectInfo) = runOnUiThread {
connected = false
Log.i("EasySDK", "disconnected")
}
}
private val errorListener = object : WuKongEventListener {
override fun onEvent(error: WuKongError) = runOnUiThread {
Log.e("EasySDK", "SDK operation failed")
}
}
fun connect(bootstrap: IMBootstrap) {
val config = WuKongConfig.Builder()
.serverUrl(bootstrap.websocketUrl)
.uid(bootstrap.uid)
.token(bootstrap.token)
.connectionTimeout(15_000)
.requestTimeout(15_000)
.maxReconnectAttempts(5)
.debugLogging(false)
.build()
val current = easySDK.getConfig()
if (current == null) {
easySDK.init(applicationContext, config)
} else {
check(
current.uid == bootstrap.uid &&
current.token == bootstrap.token &&
current.serverUrl == bootstrap.websocketUrl &&
current.connectionTimeoutMs == config.connectionTimeoutMs &&
current.requestTimeoutMs == config.requestTimeoutMs &&
current.maxReconnectAttempts == config.maxReconnectAttempts &&
current.deviceFlag == config.deviceFlag &&
current.debugLogging == config.debugLogging
) {
"WuKongEasySDK v1.0.5 cannot apply changed identity or configuration; " +
"stop and restart the process or upgrade the SDK"
}
}
registerListeners()
connected = easySDK.isConnected()
if (connected) return
lifecycleScope.launch {
runCatching { withTimeout(20_000) { easySDK.connect() } }
.onFailure {
connected = false
easySDK.disconnect()
Log.e("EasySDK", "connect failed")
}
}
}
private fun registerListeners() {
if (listenersRegistered) return
easySDK.addEventListener(WuKongEvent.CONNECT, connectListener)
easySDK.addEventListener(WuKongEvent.DISCONNECT, disconnectListener)
easySDK.addEventListener(WuKongEvent.MESSAGE, messageListener)
easySDK.addEventListener(WuKongEvent.ERROR, errorListener)
listenersRegistered = true
}
override fun onDestroy() {
if (listenersRegistered) {
easySDK.removeEventListener(WuKongEvent.CONNECT, connectListener)
easySDK.removeEventListener(WuKongEvent.DISCONNECT, disconnectListener)
easySDK.removeEventListener(WuKongEvent.MESSAGE, messageListener)
easySDK.removeEventListener(WuKongEvent.ERROR, errorListener)
listenersRegistered = false
}
if (isFinishing) easySDK.disconnect()
super.onDestroy()
}
}
```
Listeners are registered only after `init`, and the same object references are retained for `removeEventListener`. The outer `withTimeout` bounds the complete wait at 20 seconds and disconnects after failure; SDK connection and request limits are 15 seconds, with at most 5 reconnect attempts. On Activity recreation, the page reads `isConnected()` instead of waiting for a `CONNECT` event that will not be replayed. A changed identity, token, URL, or connection setting stops explicitly rather than silently retaining a stale value. The example sets `debugLogging(false)` explicitly. Maven `1.0.5` applies that master gate to protocol-parsing and event-dispatch diagnostics and redacts public model strings. A production application normally lets an Application-scoped connection manager own the SDK while Activities or Fragments subscribe only to UI events. The screen-level owner above makes the minimal lifecycle visible.
## 4. Exchange the first message [#4-exchange-the-first-message]
Add this method inside the `ChatActivity` class above and call it from your send button after connection.
```kotlin
fun sendText(targetUid: String, text: String) {
check(connected) { "EasySDK is not connected" }
lifecycleScope.launch {
val payload: String = org.json.JSONObject()
.put("type", 1)
.put("version", 1)
.put("content", text)
.toString()
runCatching {
easySDK.send(
channelId = targetUid,
channelType = WuKongChannelType.PERSON,
payload = payload,
)
}.onSuccess {
Log.i("EasySDK", "SEND completed")
}.onFailure {
Log.e("EasySDK", "send failed")
}
}
}
```
A returned `send` result means Alice obtained a result for the request. Bob must independently observe the message through his `MESSAGE` listener. Do not mark the peer as “received” inside Alice's send callback.
Because the SDK is a process singleton, use two devices, two emulators, or two independent application processes:
1. Alice and Bob each obtain their own connection material from the product backend.
2. Both sides observe `CONNECT`; on failure, record only a stable error code and stage, never the original error text.
3. Alice calls `sendText("bob", ...)` and records the send result.
4. Bob checks `fromUid`, `channelId`, `messageId`, and the payload.
5. Bob sends back to Alice and proves the reverse direction.
6. Recreate the Activity and confirm listeners are not duplicated; exit the application and confirm the connection closes.
## 5. Clean up [#5-clean-up]
`onDestroy()` removes this page’s listeners and disconnects when the Activity finishes. An application connection belongs to the Application. Version `1.0.5` cannot reinitialize with another identity or configuration; never silently reuse an old Token.
## 6. Troubleshooting [#6-troubleshooting]
* **`SDK is not initialized`:** preserve the `init → addEventListener → connect` order.
* **The same UID gets a refreshed token or route and configuration changes:** `v1.0.5` has no public reset API. Stop explicitly and restart the process; do not continue with a stale token or URL.
* **The emulator connects but a physical device does not:** confirm that the address is reachable outside the host or container and have the product backend return the device-reachable WSS ingress.
* **CONNECT, SEND, or RECVACK parsing fails:** confirm the server includes the EasySDK JSON-RPC compatibility implementation. The current server accepts snake\_case fields and the README-aligned Android JSON text-string payload, as well as JSON objects and Base64 payloads, and returns snake\_case results for Android. Use APP `0`, WEB `1`, and PC `2`; do not carry forward older literals.
* **Raw JSON or `Params` still appears in Logcat with `debugLogging` disabled:** first confirm Gradle resolved `1.0.5`, clean the old build, then use a non-production canary to distinguish SDK output from application or third-party network logs. Stop the rollout and file a minimal reproduction if the Release artifact still exposes that canary.
* **Messages are duplicated:** prevent repeated listener registration and deduplicate by `messageId`.
## Next [#next]
Continue with [messaging](/en/guide/integration/messaging) and [production checks](/en/guide/integration/acceptance). For offline recovery, conversations, unread counts, or push, see [SDK selection](/en/sdk). [Versions and validation records](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md#android) retain the exact environments and scope of past runs.
# C++ quickstart (/en/sdk/easy/cpp/getting-started)
Use C++17, CMake, and WuKongEasySDK **0.1.0** for online messaging. The main path uses the WuKongIM-maintained vcpkg registry; alternative installation methods follow the tutorial.
## 1. Prepare [#1-prepare]
Follow [Authentication & Tokens](/en/guide/integration/authentication) to obtain each user's `uid`, `token`, and `websocketUrl` from your trusted application backend. Clients connect only to Gateway and never call WuKongIM HTTP API management endpoints. Wire device flags are APP `0`, WEB `1`, PC/Desktop `2`; C++ defaults to Desktop, so the backend must store the Token under the same device category.
The default development server example uses `ws://127.0.0.1:5200` with path `/`. Use `ws://127.0.0.1:5200/ws` only when your listener or proxy is configured for `/ws`. Across machines, use a client-reachable address; production uses `wss://`. Shared setup is in [Run Official Examples](/en/sdk/easy/examples).
Use [the shared example setup](/en/sdk/easy/examples) for the server, test accounts, and reachable addresses, then continue with installation below.
## 2. Install the SDK [#2-install-the-sdk]
Install [vcpkg](https://learn.microsoft.com/en-us/vcpkg/get_started/get-started)
and set `VCPKG_ROOT` to its directory. Keep Git, CMake 3.20+ and a C++17 compiler
available (Visual Studio 2022 on Windows, Xcode command-line tools on macOS,
GCC/Clang on Linux). The tested vcpkg revision is
`04a9d8e5212d01ee1dd9478eadd9caade4f8b0d4`.
In your application directory, create `vcpkg.json`:
```json
{"dependencies": ["wukong-easy-sdk"]}
```
`vcpkg-configuration.json`:
```json
{
"default-registry": {
"kind": "git",
"repository": "https://github.com/microsoft/vcpkg",
"baseline": "04a9d8e5212d01ee1dd9478eadd9caade4f8b0d4"
},
"registries": [
{
"kind": "git",
"repository": "https://github.com/WuKongIM/WuKongEasySDK-CPP.git",
"baseline": "63ec99d34c7605b64e2173d201639042e0e49de9",
"packages": [
"wukong-easy-sdk"
]
}
]
}
```
Add the following `CMakeLists.txt` next to your own `main.cpp`:
```cmake
cmake_minimum_required(VERSION 3.20)
project(my_app LANGUAGES CXX)
find_package(WuKongEasySDK 0.1 CONFIG REQUIRED)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE WuKongEasySDK::WuKongEasySDK)
```
vcpkg installs the SDK, Boost, OpenSSL and JSON automatically. The first build
may compile dependencies and take several minutes; this is not a prebuilt
archive. The SDK port is a static library; on Windows, distribute dependency
DLLs copied beside the application when using `x64-windows`.
This public Git registry is maintained by WuKongIM in this repository. It is
not Microsoft's curated catalog: copy the registry configuration as well as
the dependency manifest. SDK source is pinned to
`3e367a908f42385ab9306f9708b7456399cace7d`, independently of the registry baseline.
Commit both JSON files to reproduce dependency selection. Existing projects
should merge these entries into their manifests instead of overwriting them.
[Independent consumer example](https://github.com/WuKongIM/WuKongEasySDK-CPP/tree/main/examples/vcpkg-consumer) · [Registry maintenance](https://github.com/WuKongIM/WuKongEasySDK-CPP/blob/main/docs/VCPKG.md)
## 3. Connect and listen [#3-connect-and-listen]
Save the complete program below as `main.cpp`. Set your own `WKIM_URL`, `WKIM_UID`, and `WKIM_TOKEN`; the first argument is the peer UID.
```cpp
#include
#include
#include
int main(int argc, char** argv) {
const char* token = std::getenv("WKIM_TOKEN");
const char* url = std::getenv("WKIM_URL");
const char* uid = std::getenv("WKIM_UID");
if (!token || !url || !uid || argc != 2) return 2;
try {
wukong::Options options;
options.connectionTimeout = std::chrono::seconds(10);
options.requestTimeout = std::chrono::seconds(15);
wukong::WKIM im(url, {uid, token}, options);
auto messageListener = im.on(wukong::WKIMEvent::Message,
[](const wukong::Json& message) {
// Copy message.at("payload") into your application's UI/event queue.
// This callback runs on the SDK I/O thread.
(void)message;
std::cout << "Message received\n";
});
auto errorListener = im.on(wukong::WKIMEvent::Error,
[](const wukong::Json&) {
// Dispatch a sanitized failure state to the application.
});
im.connect().get();
std::cout << "Connected. Start the peer, then press Enter to send.\n";
std::cin.get();
wukong::SendOptions sendOptions;
// Supply a stable clientMsgNo when the application needs reconciliation.
auto ack = im.send(argv[1], wukong::WKIMChannelType::Person,
{{"type", 1}, {"content", "Hello from C++!"}},
sendOptions).get();
if (ack.reasonCode == 1) std::cout << "SEND completed\n";
std::cin.get();
im.off(messageListener);
im.off(errorListener);
im.disconnect().get();
im.destroy().get();
} catch (const wukong::Error&) {
std::cerr << "EasySDK operation failed\n";
return 1;
}
}
```
`connect()` succeeds only after authentication. For group messages, use `WKIMChannelType::Group` after your backend creates the Channel and membership. Payload accepts a JSON object or array, encoded as Base64 UTF-8 JSON. Incoming objects, JSON text, and Base64 JSON are supported. Message IDs stay strings to preserve 64-bit precision.
Send results contain `messageId`, `messageSeq`, and `reasonCode`. Receive events additionally contain `header`, second-based `timestamp`, `channelId`, `channelType`, `fromUid`, and `payload`. Automatic RECVACK includes `messageId` and `messageSeq`; this is a transport receipt, not a business read receipt. See [Messaging](/en/guide/integration/messaging) for the distinction between send completion, receipt, and application processing.
## 4. Exchange the first message [#4-exchange-the-first-message]
```sh
# Linux / macOS
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release --parallel 2
```
```powershell
# Windows / Visual Studio 2022
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows
cmake --build build --config Release --parallel 2
```
After building, run in two terminals with their respective credentials:
```sh
# Alice
./build/my_app bob
# Bob
./build/my_app alice
```
On Windows use `build/Release/my_app.exe`. When both show `Connected`, press Enter on each to send. Check `SEND completed` and the peer’s `Message received`; press Enter again after checking both directions to exit.
Once both clients are connected, Alice sends to `bob` and Bob checks the sender and content in the message callback. Then Bob sends to `alice`. A send result means the server accepted the request; it is not recipient delivery or a read receipt.
## 5. Clean up [#5-clean-up]
Each instance owns one identity and one I/O thread, with no global singleton. Public operations support concurrent application threads; events are dispatched serially on the I/O thread. A callback may enqueue an asynchronous operation, but **must never wait on an SDK future**. Dispatch UI updates and slow work to your application's executor.
| Operation | Semantics |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| `on(...)` / `off(listenerId)` | Retain and remove listener IDs; callbacks already selected for dispatch may still finish |
| `connect()` | Concurrent callers share one authentication attempt; connected calls return the current result |
| `disconnect()` | Cancel pending requests, socket, heartbeat, and reconnect; reconnect is allowed afterwards |
| `destroy()` | Terminal shutdown; later operations reject; repeated calls are safe |
| Destructor | Initiates cleanup and joins the thread; callback-owned destruction exits the thread after callback completion |
| Change identity, Token, or URL | Shut down the old instance and create a new one |
Captured application state must outlive client shutdown. Capture the client through `weak_ptr` to avoid ownership cycles. Removing a listener alone does not permit freeing captured state until shutdown finishes. Socket cancellation bounds cleanup without waiting for the peer's close handshake.
## 6. Troubleshooting [#6-troubleshooting]
Defaults are a 10-second total connection deadline, 15-second request deadline, 25-second ping interval, and 10-second pong timeout. A same-ID `result: null` acknowledges heartbeat. After a previously authenticated connection is lost, retry at most five times with exponential delay from 1 second to a 30-second cap and jitter. Initial connect failures return to the caller; authentication rejection, server-requested disconnect, malformed protocol, and manual exit stop automatic retries.
Defaults allow 1,024 pending requests, separate 4 MiB command and WebSocket write queues, and 1 MiB per wire message. Capacity failures use `ErrorCode::QueueFull`; local errors are negative, while server reason codes are preserved by `Error::code()`. A timeout or lost connection can leave send delivery unknown; reconcile through `clientMsgNo`. The SDK does not queue offline or automatically resend.
WSS verifies the certificate chain and hostname and requires TLS 1.2+. Use `Options::caFile` for a private PEM CA; the console also reads `WKIM_CA_FILE`. There is no certificate-verification bypass. The SDK is default-silent and never logs Tokens, payloads, URLs, raw frames, server response text, or underlying error objects.
Receive `id`, `type`, millisecond-based `timestamp`, and `data` through `WKIMEvent::CustomEvent`; JSON-string data is parsed. Event reception depends on the server producing that notification.
## Alternative installation: prebuilt archives [#alternative-installation-prebuilt-archives]
To skip dependency compilation, download the matching ZIP and `SHA256SUMS` from [C++ SDK v0.1.0 Release](https://github.com/WuKongIM/WuKongEasySDK-CPP/releases/tag/v0.1.0), verify its SHA-256 and extract it. You only need CMake 3.20+ and a compatible C++ development environment; no separate vcpkg installation is required.
| Archive suffix | Consumer environment |
| ---------------------------- | ----------------------------------------------------------------- |
| `linux-x64-gcc13.zip` | Ubuntu 24.04 x64, GCC 13, libstdc++ C++11 ABI, glibc 2.39+ |
| `macos-arm64-appleclang.zip` | macOS 14+ arm64, Apple Clang, libc++ |
| `windows-x64-msvc143-md.zip` | Windows x64, Visual Studio 2022 v143; Release `/MD`, Debug `/MDd` |
Names start with `WuKongEasySDK-CPP-0.1.0-`. Each archive includes Debug/Release static SDK libraries, Boost/JSON headers, OpenSSL libraries, licenses and a minimal example. From the extracted directory:
```sh
# Linux / macOS
cmake -S example -B build -DCMAKE_TOOLCHAIN_FILE="$PWD/wukong-sdk.cmake" -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release --parallel 2
ctest --test-dir build -C Release --output-on-failure
```
```powershell
# Windows / Visual Studio 2022
cmake -S example -B build -A x64 -DCMAKE_TOOLCHAIN_FILE="$PWD/wukong-sdk.cmake"
cmake --build build --config Release --parallel 2
ctest --test-dir build -C Release --output-on-failure
```
`wukong_example` verifies initialization and destruction; `wukong_chat` supports interactive messaging with the Alice/Bob credentials below. For your application, retain the `find_package` and `target_link_libraries` above and point CMake's toolchain option to the extracted `wukong-sdk.cmake`.
OpenSSL is static on Unix and uses bundled DLLs on Windows. Deploy Windows applications with the DLLs CMake copies beside the executable and a compatible Visual C++ Redistributable. Debug runtimes are for development only. **For WSS with prebuilt packages, supply a maintained CA bundle explicitly via `Options::caFile` (`WKIM_CA_FILE` in the chat example)**; do not depend on OpenSSL's build-machine default certificate path.
`BUILD_INFO.json` identifies the SDK source, registry and packaging commits; `FILES.sha256.json` verifies extracted content. Upgrade into a separate directory, check hashes, rebuild in a new build directory and rerun acceptance. Update the application and dependencies together and retain the previous version for rollback. Use vcpkg/source for other compilers, architectures, CRTs or dependency combinations; binary dependencies cannot be mixed arbitrarily.
Fresh jobs download each platform's ZIP and compile Debug/Release consumers at a different path, running lifecycle checks and 26 WS/WSS scenarios per configuration. Linux/macOS also use the pinned WuKongIM server to verify bidirectional messaging, reconnect and presence cleanup. Windows evidence uses the protocol fixture, not a Windows server. See [prebuilt package documentation](https://github.com/WuKongIM/WuKongEasySDK-CPP/blob/v0.1.0/docs/PREBUILT.md) for compatibility and release acceptance.
## Alternative installation: build from source [#alternative-installation-build-from-source]
Requirements: CMake 3.20+, a C++17 compiler, Boost 1.74+, OpenSSL 1.1.1+, and nlohmann/json 3.11+. Products should use maintained dependency versions with current security fixes.
```sh
git clone https://github.com/WuKongIM/WuKongEasySDK-CPP.git
cd WuKongEasySDK-CPP
git checkout 3e367a908f42385ab9306f9708b7456399cace7d
# macOS
brew install cmake boost openssl@3 nlohmann-json
# Ubuntu / Debian
sudo apt-get install g++ cmake libboost-dev libssl-dev nlohmann-json3-dev
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release --parallel 2
ctest --test-dir build -C Release --output-on-failure
```
Run only the dependency command for your platform. Without an installed JSON package, CMake fetches the exact upstream 3.11.3 commit. For offline builds, preinstall dependencies and set `WUKONG_FETCH_JSON=OFF`.
Windows uses Visual Studio 2022 and vcpkg; the repository's `vcpkg.json` pins the baseline:
```powershell
git -C "$env:VCPKG_ROOT" fetch origin 04a9d8e5212d01ee1dd9478eadd9caade4f8b0d4
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows
cmake --build build --config Release --parallel 2
ctest --test-dir build -C Release --output-on-failure
```
Add the source to your application's CMake:
```cmake
add_subdirectory(external/WuKongEasySDK-CPP)
target_link_libraries(my_app PRIVATE WuKongEasySDK::WuKongEasySDK)
```
Alternatively, run `cmake --install build --config Release --prefix /path/to/sdk-prefix`, use `find_package(WuKongEasySDK 0.1 CONFIG REQUIRED)` downstream, and point `CMAKE_PREFIX_PATH` at the installation. The static-library export preserves its dependencies; Windows distribution must include any required dynamic dependencies.
## Run the archive or source chat example [#run-the-archive-or-source-chat-example]
In two terminals, supply each user's backend-issued Token through `WKIM_TOKEN`, then run:
```sh
# Alice's terminal
./build/wukong_chat ws://127.0.0.1:5200 alice bob
# Bob's terminal
./build/wukong_chat ws://127.0.0.1:5200 bob alice
```
After both print `Connected`, enter text, observe `Message` on the other side, and send a reply. `SEND completed` means SENDACK succeeded. Enter `/quit` to disconnect and release resources. On Windows, use `build/Release/wukong_chat.exe`.
The example intentionally displays business message content; the SDK itself emits no logs. Do not pipe the example terminal into production log collection.
## Next [#next]
Continue with [messaging](/en/guide/integration/messaging) and [production checks](/en/guide/integration/acceptance). For offline recovery, conversations, unread counts, or push, see [SDK selection](/en/sdk). [Versions and validation records](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md#cpp) retain the exact environments and scope of past runs.
# C# quickstart (/en/sdk/easy/csharp/getting-started)
Use .NET 8+ and NuGet **1.0.0** to exchange online messages between two independent console clients.
## 1. Prepare [#1-prepare]
* Prepare .NET 8 or later on Windows, Linux, or macOS. The target framework is `net8.0`; Unity, .NET Framework, and browser WebAssembly are not currently supported.
* Start a WuKongIM single-node cluster or multi-node cluster with a healthy `/readyz` and a reachable WebSocket Gateway. A single-node cluster still follows cluster semantics and defaults to 256 hash slots.
* Have your trusted backend supply separate `uid`, `token`, and `websocketUrl` values for Alice and Bob. Clients do not call WuKongIM HTTP API management routes such as `/user/token` or `/route`.
* C# defaults to PC/Desktop `2`. The backend token's `device_flag` must match the client; APP is `0` and Web is `1`.
Read [Identity & Token](/en/guide/integration/authentication) first. Production uses HTTPS/WSS with certificate validation. Tokens do not belong in URLs, logs, or source code.
After product login, retrieve this minimal response through a protected product route:
```json
{
"uid": "alice",
"token": "backend-issued-desktop-token",
"websocketUrl": "wss://im.example.com/ws"
}
```
The console example reads these values from `WUKONGIM_WS_URL`, `WUKONGIM_UID`, and `WUKONGIM_TOKEN`. A desktop application can consume the login response directly. These variables belong to the example; they are not the server's `WK_` configuration keys.
Use [the shared example setup](/en/sdk/easy/examples) for the server, test accounts, and reachable addresses, then continue with installation below.
## 2. Install the SDK [#2-install-the-sdk]
```bash
dotnet new console -n MyChat --framework net8.0
dotnet add MyChat/MyChat.csproj package WuKongEasySDK --version 1.0.0 --source https://api.nuget.org/v3/index.json
```
## 3. Connect and listen [#3-connect-and-listen]
Replace `MyChat/Program.cs` with the following. Set the three environment variables for the current identity and supply the other user's UID as a command-line argument.
```csharp
using WuKongEasySDK;
static string Required(string name) =>
Environment.GetEnvironmentVariable(name)
?? throw new InvalidOperationException($"Missing {name}");
if (args.Length != 1)
throw new ArgumentException("Pass the peer UID as the first argument.");
await using var im = new WKIM(Required("WUKONGIM_WS_URL"), new AuthOptions
{
Uid = Required("WUKONGIM_UID"),
Token = Required("WUKONGIM_TOKEN"),
DeviceFlag = DeviceFlag.Desktop
}, new WKIMOptions
{
ConnectTimeout = TimeSpan.FromSeconds(10),
RequestTimeout = TimeSpan.FromSeconds(15)
});
Action onMessage = message =>
{
// Deduplicate by MessageId and pass Payload to application state.
// Marshal WinForms/WPF updates to the UI thread; do not log full messages.
Console.WriteLine("Message received.");
};
im.Message += onMessage;
im.Connected += _ => Console.WriteLine("Connected.");
im.Disconnected += _ => Console.WriteLine("Disconnected.");
im.Error += _ => Console.WriteLine("SDK operation failed.");
im.CustomEvent += notification =>
{
// Route notification.Data using notification.Type.
};
try
{
await im.ConnectAsync();
Console.WriteLine("Start the peer, then press Enter to send.");
Console.ReadLine();
var result = await im.SendAsync(args[0], ChannelType.Person,
new { type = 1, content = "Hello from C# 👋" });
if (!result.IsSuccess)
Console.WriteLine($"SEND rejected: {(int)result.ReasonCode}");
else
Console.WriteLine("Server accepted SEND.");
Console.WriteLine("Press Enter after checking both directions to exit.");
Console.ReadLine();
}
finally
{
im.Message -= onMessage;
await im.DisconnectAsync();
}
```
In separate terminals using Alice and Bob's connection material, run `dotnet run --project MyChat -- bob` and `dotnet run --project MyChat -- alice`. Async methods return exceptions to their callers and background failures also raise `Error`; provide application handling for authentication failures, timeouts, and business rejections.
SDK logging is disabled by default. Setting `WKIMOptions.DebugLogger` enables only fixed operational strings, excluding tokens, payloads, raw frames, and server error bodies.
## 4. Exchange the first message [#4-exchange-the-first-message]
Once both clients are connected, Alice sends to `bob` and Bob checks the sender and content in the message callback. Then Bob sends to `alice`. A send result means the server accepted the request; it is not recipient delivery or a read receipt.
Use `ChannelType.Group` with a backend-managed group ID for group messages. Membership and permissions remain server decisions. `SendOptions` provides `ClientMsgNo`, `Header`, `Setting`, and `Topic`. The default header sets `RedDot = true`; an explicitly supplied header is respected.
`ReasonCode.Success` is **1**. `SendResult.IsSuccess` means the server accepted SEND, not that Bob received, displayed, or read it. Business rejection codes such as `128–255` remain in the result. JSON-RPC errors throw `WKIMRpcException` with a numeric `Code`.
Message IDs use `string`; numeric wire IDs never pass through floating-point conversion. `MessageSeq` and `NodeId` use `ulong`. Message `Timestamp` is Unix **seconds**; custom event `Timestamp` is Unix **milliseconds**.
`RecvMessage.Payload` and `EventNotification.Data` are independently owned `JsonElement` values. Receive payloads accept JSON objects or Base64 JSON; undecodable strings remain unchanged. Custom event JSON strings become JSON values, while plain strings remain strings.
## 5. Clean up [#5-clean-up]
| Behavior | C# SDK contract |
| ---------------------- | --------------------------------------------------------------------------------------- |
| Instance ownership | `new WKIM` / `WKIM.Init` creates an independent instance; no implicit global singleton |
| Concurrent connects | `ConnectAsync` callers share the attempt; canceling one cancels only its wait |
| Stop shared connection | Await `DisconnectAsync` before connecting again |
| Initial failure | WebSocket open and authentication share a 10-second budget; failure returns directly |
| Loss after connection | Up to five retries by default, at 1, 2, 4, 8, and 16 seconds; success resets the budget |
| Stop automatic retry | Authentication rejection, server disconnect/kick, manual disconnect, or disposal |
| Final cleanup | Await `DisposeAsync` from lifecycle code, or use `await using` |
For token rotation or account switching, dispose the old instance and create one with fresh backend-issued credentials. Set `AuthOptions.DeviceId` explicitly when the device identifier should persist across processes.
Events run serially in the background. Keep handlers short; do not synchronously wait for SDK async operations or wait for disposal inside a callback. A throwing handler does not prevent other listeners or automatic ACK. Already queued callbacks may finish after listener removal or disconnect; `DisposeAsync` waits for them to drain.
RECVACK means admission to the SDK receive queue, not successful business processing. Defaults are 256 pending requests, 128 queued events, and 1 MiB per complete JSON-RPC envelope. Excess requests throw `WKIMBackpressureException`; a full event queue closes the connection without ACKing the unaccepted message. Configure these bounds through `WKIMOptions`.
The SDK does not queue offline messages or automatically resend SEND. A timeout, cancellation, or disconnect may leave the send outcome uncertain. Reconcile business state first; reuse `SendOptions.ClientMsgNo` when retrying the same logical message. Your application owns offline recovery, conversations, unread counts, push, and business receipts.
## 6. Troubleshooting [#6-troubleshooting]
**Does reconnect select another address?** Each instance retries its fixed URL. Let the backend choose a live ingress, await `DisposeAsync` on the old instance, and create a replacement. Complete three-node fault recovery still has [known server issues](https://github.com/WuKongIM/WuKongIM/issues/927); single-node messaging does not establish continuous availability during cluster faults.
**Can I retry a timed-out send?** Its result may be unknown. Reconcile first, then follow the `ClientMsgNo` idempotency contract.
**Why does a UI update fail?** Events dispatch in the background; marshal WinForms/WPF updates to the UI thread.
## Alternative installation: source and local packages [#alternative-installation-source-and-local-packages]
```bash
git clone https://github.com/WuKongIM/WuKongEasySDK-CSharp.git
git -C WuKongEasySDK-CSharp checkout 02ea7d60cd94feef1996f41bca35ffc3b8e18ea6
dotnet new console -n MyChat --framework net8.0
dotnet add MyChat/MyChat.csproj reference WuKongEasySDK-CSharp/src/WuKongEasySDK/WuKongEasySDK.csproj
```
The two directories are siblings. Record the exact source revision in your build configuration or maintain the reference through a submodule pinned to that commit.
For a local NuGet installation, build the package from that pinned checkout:
```bash
cd WuKongEasySDK-CSharp
dotnet pack src/WuKongEasySDK -c Release -o artifacts
dotnet add ../MyChat/MyChat.csproj package WuKongEasySDK --version 1.0.0 --source ./artifacts
```
Choose one of the public package, project reference, or local package. Do not combine them. Here, `--source ./artifacts` is the local feed you just built.
## Next [#next]
Continue with [messaging](/en/guide/integration/messaging) and [production checks](/en/guide/integration/acceptance). For offline recovery, conversations, unread counts, or push, see [SDK selection](/en/sdk). [Versions and validation records](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md#csharp) retain the exact environments and scope of past runs.
# Flutter quickstart (/en/sdk/easy/flutter/getting-started)
This tutorial uses **1.1.0** to exchange online messages between Alice and Bob in separate clients. WebSocket JSON-RPC CONNECT authenticates the connection.
## 1. Prepare [#1-prepare]
Prepare the following:
* Flutter 3.0 or later.
* Dart 3.0 or later.
* A WuKongIM single-node cluster or multi-node cluster whose `/readyz` is healthy and whose WebSocket Gateway is reachable from the target device.
* A product backend that returns `uid`, a short-lived `token`, and `websocketUrl` separately for Alice and Bob.
Read [Identity & Token](/en/guide/integration/authentication) first. If the application builds for mobile, desktop, and Web, accept each target independently; evidence from one target does not prove all targets.
To see messages first, follow [the official example](/en/sdk/easy/examples#flutter) with two clients. The steps below integrate the SDK into your application.
The default device category is **APP `0`**. Store the Token with the same `device_flag` on your backend: APP is `0`, WEB is `1`, and PC is `2`.
## 2. Install the SDK [#2-install-the-sdk]
Add the exact version to `pubspec.yaml`:
```yaml
dependencies:
flutter:
sdk: flutter
wukong_easy_sdk: 1.1.0
```
Then run:
```bash
flutter pub get
```
Commit the updated `pubspec.lock` so CI and developer machines install the same version.
## 3. Connect and listen [#3-connect-and-listen]
```dart
class IMBootstrap {
const IMBootstrap({
required this.uid,
required this.token,
required this.websocketUrl,
});
final String uid;
final String token;
final String websocketUrl;
}
```
The product backend validates product login before returning these fields. The client holds no WuKongIM HTTP API management credential, and production uses `wss://`.
The following screen starts initialization once from `initState`. Each callback is stored in a field so the identical function reference can be passed back from `dispose`.
```dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:wukong_easy_sdk/wukong_easy_sdk.dart';
class ChatPage extends StatefulWidget {
const ChatPage({required this.bootstrap, super.key});
final IMBootstrap bootstrap;
@override
State createState() => _ChatPageState();
}
class _ChatPageState extends State {
final easySDK = WuKongEasySDK.getInstance();
final messagesById = {};
late final WuKongEventListener connectListener;
late final WuKongEventListener disconnectListener;
late final WuKongEventListener messageListener;
late final WuKongEventListener errorListener;
bool listenersRegistered = false;
bool connected = false;
@override
void initState() {
super.initState();
_createListeners();
_start();
}
void _createListeners() {
connectListener = (result) {
if (!mounted) return;
setState(() => connected = true);
debugPrint('connected');
};
disconnectListener = (info) {
if (!mounted) return;
setState(() => connected = false);
debugPrint('disconnected');
};
messageListener = (message) {
if (!mounted) return;
setState(() {
messagesById[message.messageId] = message;
if (messagesById.length > 100) {
messagesById.remove(messagesById.keys.first);
}
});
};
errorListener = (error) {
debugPrint('EasySDK operation failed');
};
}
Future _start() async {
final config = WuKongConfig(
serverUrl: widget.bootstrap.websocketUrl,
uid: widget.bootstrap.uid,
token: widget.bootstrap.token,
debugLogging: false,
);
try {
await easySDK.init(config);
if (!mounted) {
easySDK.dispose();
return;
}
_registerListeners();
await easySDK.connect().timeout(
const Duration(seconds: 20),
onTimeout: () {
easySDK.disconnect();
throw TimeoutException('EasySDK connect exceeded 20 seconds');
},
);
} catch (_) {
easySDK.disconnect();
debugPrint('EasySDK connect failed');
}
}
void _registerListeners() {
if (listenersRegistered) return;
easySDK.addEventListener(WuKongEvent.connect, connectListener);
easySDK.addEventListener(WuKongEvent.disconnect, disconnectListener);
easySDK.addEventListener(WuKongEvent.message, messageListener);
easySDK.addEventListener(WuKongEvent.error, errorListener);
listenersRegistered = true;
}
@override
void dispose() {
if (listenersRegistered) {
easySDK.removeEventListener(WuKongEvent.connect, connectListener);
easySDK.removeEventListener(WuKongEvent.disconnect, disconnectListener);
easySDK.removeEventListener(WuKongEvent.message, messageListener);
easySDK.removeEventListener(WuKongEvent.error, errorListener);
}
easySDK.disconnect();
easySDK.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(connected ? 'Online' : 'Connecting')),
body: ListView(
children: messagesById.values
.map((message) => ListTile(title: Text(_displayPayload(message.payload))))
.toList(),
),
);
}
String _displayPayload(dynamic payload) {
if (payload is String) {
try {
return utf8.decode(base64Decode(payload));
} catch (_) {
return payload; // Preserve an unknown/non-Base64 payload for fallback UI.
}
}
return jsonEncode(payload);
}
}
```
`debugLogging: false` matches the SDK default and is written explicitly here so production configuration can be reviewed. Set it to `true` only during a controlled diagnostic window. When a `logHandler` is also supplied, it receives only SDK-sanitized operational metadata; application code must still avoid appending complete events, models, or payloads.
Page-level `dispose()` is appropriate for the minimal example. `Future.timeout` bounds the complete connection wait at 20 seconds and disconnects after timeout or another failure; the page must not remain in Connecting forever. If a real application keeps the connection while navigating, let Provider, Riverpod, Bloc, or another application-level state owner hold the SDK. A page adds and removes only its own listeners; final logout performs `disconnect` and `dispose`.
## 4. Exchange the first message [#4-exchange-the-first-message]
Add this method to `_ChatPageState`:
```dart
Future sendText(String targetUid, String text) async {
if (!connected) throw StateError('EasySDK is not connected');
await easySDK.send(
channelId: targetUid,
channelType: WuKongChannelType.person,
payload: {
'type': 1,
'version': 1,
'content': text,
},
);
debugPrint('SEND completed');
}
```
After `sendText('bob', 'Hello from Flutter EasySDK')`, Alice records `SendResult`. Bob must independently see the message in his `messageListener`; send result and realtime receipt are different events.
1. Start Alice and Bob on two devices, two simulators, or two independent browser contexts.
2. Enable send only after both sides observe `WuKongEvent.connect`.
3. Have Alice send to the person Channel `bob`, recording ID, sequence, and Reason Code.
4. On Bob, verify `fromUid` and `channelId`, inspect `type` and `content` in the object payload, and deduplicate by `messageId`.
5. Have Bob send to Alice and prove the reverse direction.
6. Destroy and reopen the page, confirm no duplicate listener remains, then verify final resource release on logout.
## 5. Clean up [#5-clean-up]
Page disposal removes listeners, calls `disconnect()`, and calls `dispose()`. To keep the connection across pages, move ownership into application state and release it on logout. The example displays at most the latest 100 messages.
## 6. Troubleshooting [#6-troubleshooting]
* **Initialization or a UID switch leaves stale state:** wait for `easySDK.init(config)` before connecting. Before switching accounts, remove listeners, disconnect, and `dispose`, then clear product-local state.
* **Messages duplicate after Widget rebuild:** never register listeners from `build`; retain callbacks and remove them from `dispose`.
* **System logs still contain a token or payload:** first confirm the lockfile actually resolves `wukong_easy_sdk 1.1.0`, then inspect application callbacks, `debugPrint`, a custom `logHandler`, and collectors for complete event or model logging. SDK diagnostics are off by default and should emit only sanitized operational metadata when explicitly enabled. Reproduce with the Release artifact and retain only low-sensitivity evidence before filing an issue.
* **`message.payload` is a string:** the current server emits valid JSON objects as objects. If a string remains, confirm the server revision, then try Base64 → UTF-8 JSON and preserve unknown formats for fallback UI.
* **A physical device cannot reach the local address:** `localhost` means the device itself; have the product backend return a device-reachable WSS ingress.
* **Send succeeds but the peer message is absent from the UI:** inspect send result, realtime receipt, and offline recovery separately.
## Next [#next]
Continue with [messaging](/en/guide/integration/messaging) and [production checks](/en/guide/integration/acceptance). For offline recovery, conversations, unread counts, or push, see [SDK selection](/en/sdk). [Versions and validation records](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md#flutter) retain the exact environments and scope of past runs.
# Web quickstart (/en/sdk/easy/javascript/getting-started)
Use **2.0.5** to exchange online messages between Alice and Bob in separate clients.
## 1. Prepare [#1-prepare]
| Item | Requirement |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Browser | WebSocket, `TextEncoder`, and `TextDecoder` |
| WuKongIM cluster | Single-node or multi-node cluster; healthy `/readyz` and a browser-reachable WebSocket Gateway |
| Application BFF | Validates the product session and returns the current user's `uid`, short-lived `token`, and `websocketUrl`; same origin or deliberate cross-origin policy |
Start the test page through [the official example](/en/sdk/easy/examples#javascript). Use temporary test accounts and separate Alice and Bob clients. The code below integrates your own application.
① WebSocket address. ② Current UID, with its matching temporary token. ③ Connect. This is the official test tool; production applications obtain credentials from a BFF.
Read [Identity & Token](/en/guide/integration/authentication). Use HTTPS/WSS in production and keep tokens out of URLs, Local Storage, logs, and analytics. The default device category is **WEB `1`**; backend `device_flag` must match (APP `0`, PC `2`).
## 2. Install the SDK [#2-install-the-sdk]
Run in a bundled application such as Vite or Next.js, then commit the lockfile:
```bash
npm install --save-exact easyjssdk@2.0.5
```
## 3. Connect and listen [#3-connect-and-listen]
`/api/im/bootstrap` is **your product route**, not WuKongIM HTTP API. The browser never calls `/user/token` or `/route` directly; server credentials, route selection, and response filtering stay in the BFF.
Place the following three code blocks in one TS module, in order: fetch connection material, create the connection owner, then bind the page.
① Fetch BFF connection material
```ts
interface IMBootstrap {
uid: string;
token: string;
websocketUrl: string;
}
async function fetchIMBootstrap(): Promise {
const response = await fetch('/api/im/bootstrap', {
credentials: 'include',
cache: 'no-store',
headers: { accept: 'application/json' },
});
if (!response.ok) throw new Error(`IM bootstrap failed: ${response.status}`);
return response.json() as Promise;
}
```
② Complete connection wrapper: listeners, 10-second limit, and destruction
```ts
import { WKIM, WKIMChannelType, WKIMEvent, type RecvMessage } from 'easyjssdk';
export class EasyChatClient {
private im?: WKIM;
constructor(private readonly receiveMessage: (message: RecvMessage) => void) {}
private readonly onConnect = (_result: unknown) => {
console.info('EasySDK connected');
};
private readonly onDisconnect = (_info: unknown) => {
console.info('EasySDK disconnected');
};
private readonly onMessage = (message: RecvMessage) => {
this.receiveMessage(message);
};
private readonly onError = (_error: unknown) => {
console.error('EasySDK operation failed');
};
async start(bootstrap: IMBootstrap): Promise {
this.stop();
const im = WKIM.init(
bootstrap.websocketUrl,
{ uid: bootstrap.uid, token: bootstrap.token },
{ singleton: false, debugLogging: false },
);
im.on(WKIMEvent.Connect, this.onConnect);
im.on(WKIMEvent.Disconnect, this.onDisconnect);
im.on(WKIMEvent.Message, this.onMessage);
im.on(WKIMEvent.Error, this.onError);
this.im = im;
await this.connectWithin(im, 10_000);
}
private async connectWithin(im: WKIM, timeoutMs: number): Promise {
let timeout: ReturnType | undefined;
try {
await Promise.race([
im.connect(),
new Promise((_, reject) => {
timeout = setTimeout(
() => reject(new Error(`EasySDK connect exceeded ${timeoutMs} ms`)),
timeoutMs,
);
}),
]);
} catch (error) {
im.destroy();
if (this.im === im) this.im = undefined;
throw error;
} finally {
if (timeout) clearTimeout(timeout);
}
}
async sendText(targetUid: string, text: string) {
if (!this.im?.isConnected) throw new Error('EasySDK is not connected');
return this.im.send(targetUid, WKIMChannelType.Person, {
type: 1,
version: 1,
content: text,
});
}
stop(): void {
const im = this.im;
if (!im) return;
im.off(WKIMEvent.Connect, this.onConnect);
im.off(WKIMEvent.Disconnect, this.onDisconnect);
im.off(WKIMEvent.Message, this.onMessage);
im.off(WKIMEvent.Error, this.onError);
im.destroy();
this.im = undefined;
}
}
```
`on` and `off` use the same function reference. `singleton: false` prevents hot reload or multiple instances from destroying each other's connections. The full connection wait is bounded at 10 seconds and calls `destroy` on failure. An application normally owns one `EasyChatClient`.
Component lifecycle and diagnostic logging
React can hold the client in a root Provider and call `stop()` during Effect cleanup. Vue/Svelte use component or application unmount hooks. CONNECT has an internal request timeout, but WebSocket opening can still hang, so bound the full wait.
`debugLogging: false` is the default. Enable it only during controlled diagnosis. The SDK emits sanitized operational metadata; application callbacks and collectors must not log complete events, results, or payloads.
## 4. Exchange the first message [#4-exchange-the-first-message]
1. Confirm **Connected** on both clients.
2. Alice sends to Bob; confirm **Message sent successfully** on the sender.
3. Confirm **Message received** on Bob; official test logs show only sequence and Channel type.
4. Bob replies to Alice; verify reception in the reverse direction.
① Target the peer UID. ② Alice's send request succeeds. ③ Alice receives Bob's reply. Check sending and peer reception separately.
A resolved send Promise means the server returned a send result, **not that the peer read or processed it**. This page verifies online exchange; see [SDK selection](/en/sdk) for offline recovery, conversations, and unread state.
③ Complete page button and receive-area example
Send after both clients connect. Alice targets `bob`; Bob changes it to `alice`. These represent your product UIDs and must match the BFF identities. In your own Message callback, check `fromUid`, `channelId`, `messageId`, and payload. Show only the latest message to avoid an unbounded history.
```html
```
```ts
const output = document.querySelector('#received-message');
const button = document.querySelector('#send-message');
if (!output || !button) throw new Error('Missing message UI');
button.disabled = true;
const chat = new EasyChatClient((message) => {
output.textContent = JSON.stringify({
fromUid: message.fromUid,
payload: message.payload,
});
});
window.addEventListener('pagehide', () => chat.stop(), { once: true });
try {
await chat.start(await fetchIMBootstrap());
button.disabled = false;
} catch {
chat.stop();
output.textContent = 'Connection failed';
}
button.onclick = async () => {
try {
// Bob sends to 'alice'. Click only after both clients are connected.
await chat.sendText('bob', 'Hello from Web EasySDK');
console.info('SEND completed');
} catch {
console.error('Send failed; check its outcome before retrying');
}
};
```
## 5. Clean up [#5-clean-up]
Call `chat.stop()` on logout, unmount, or page exit: `off` with the original callbacks, then `destroy()`. Page subscribers remove their handlers; the connection owner destroys the instance.
Additional verification: unreachable address and refresh
Confirm failure and destruction within 10 seconds with an unreachable address, without a permanent Connecting state. Refresh, close, or sign out and confirm the old instance removes listeners and is destroyed, leaving no duplicate connection.
## 6. Troubleshooting [#6-troubleshooting]
| Symptom | Check first |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Missing WebSocket or encoding APIs | Browser and build environment; browser support does not imply Mini Program support |
| Connection rejected | BFF-returned WSS address, token lifetime, certificate, proxy Upgrade, and Gateway reachability |
| Duplicate messages after refresh | Avoid `on` on every render; call `off` / `destroy` on unmount |
| Token or payload in Console | The actual SDK version in lockfile and bundle, plus application callbacks, error handling, and collectors |
Reproduce logging problems with the pinned npm artifact and retain only low-sensitivity evidence. SDK diagnostics are off by default and emit only sanitized operational metadata when enabled.
## Next [#next]
Continue with [messaging](/en/guide/integration/messaging) and [production checks](/en/guide/integration/acceptance). [Versions and validation records](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md#javascript) retain historical environments and scope; see [API Versions & Compatibility](/en/api/compatibility) for current boundaries.
Connecting a model? Follow [Agent Streaming Replies](/en/sdk/easy/agent-streaming) to update one reply and keep model keys on your backend.
# iOS quickstart (/en/sdk/easy/ios/getting-started)
This tutorial uses **1.1.1** to exchange online messages between Alice and Bob in separate clients. WebSocket JSON-RPC CONNECT authenticates the connection.
## 1. Prepare [#1-prepare]
Prepare the following:
* Set the iOS deployment target to **iOS 15 or later**. The package manifest declares iOS 13, but the public `WuKongEasySDK` class in `v1.1.1` is marked `@available(iOS 15.0, ...)`, so this tutorial uses the more conservative API limit.
* Use an Xcode toolchain that supports Swift 5.7 packages.
* Run a WuKongIM single-node cluster or multi-node cluster whose `/readyz` is healthy and whose WebSocket Gateway is reachable from the device.
* Have the product backend return `uid`, a short-lived `token`, and `websocketUrl` separately for Alice and Bob.
Read [Identity & Token](/en/guide/integration/authentication) first. Never embed a fixed token in the app, source repository, logs, or screenshots.
To see messages first, follow [the official example](/en/sdk/easy/examples#ios) with two clients. The steps below integrate the SDK into your application.
The default device category is **APP `0`**. Store the Token with the same `device_flag` on your backend: APP is `0`, WEB is `1`, and PC is `2`.
## 2. Install the SDK [#2-install-the-sdk]
### Swift Package Manager [#swift-package-manager]
In Xcode, choose **File → Add Package Dependencies** and enter:
```text
https://github.com/WuKongIM/WuKongEasySDK-iOS.git
```
Choose **Exact Version** and enter `1.1.1`. If the project owns a `Package.swift`, use an exact rule:
```swift
dependencies: [
.package(
url: "https://github.com/WuKongIM/WuKongEasySDK-iOS.git",
exact: "1.1.1"
)
]
```
## 3. Connect and listen [#3-connect-and-listen]
Map the product-backend response into an application model. The real implementation obtains this over HTTPS and does not call WuKongIM HTTP API management routes from the client:
```swift
struct IMBootstrap: Decodable {
let uid: String
let token: String
let websocketUrl: String
}
```
`websocketUrl` may use `ws://` during local development. In production, the product backend selects and returns a `wss://` address.
The following object registers listeners before connecting. On exit it removes each listener with the returned `EventListener`, preventing duplicate handling after a view is opened again.
```swift
import Combine
import Foundation
import WuKongEasySDK
@MainActor
final class EasyChatClient: ObservableObject {
@Published private(set) var isConnected = false
@Published private(set) var messages: [Message] = []
private var sdk: WuKongEasySDK?
private var listeners: [EventListener] = []
func start(with bootstrap: IMBootstrap) async throws {
stop()
let config = try WuKongConfig(
serverUrl: bootstrap.websocketUrl,
uid: bootstrap.uid,
token: bootstrap.token,
connectionTimeout: 15,
requestTimeout: 15,
maxReconnectAttempts: 5,
enableDebugLogging: false,
logLevel: .error,
enableJsonLogging: false // Also disable JSON summaries in production.
)
let sdk = WuKongEasySDK(config: config)
listeners.append(sdk.onConnect { [weak self] _ in
Task { @MainActor in self?.isConnected = true }
})
listeners.append(sdk.onDisconnect { [weak self] _ in
Task { @MainActor in
self?.isConnected = false
print("WuKongEasySDK disconnected")
}
})
listeners.append(sdk.onMessage { [weak self] message in
Task { @MainActor in
guard self?.messages.contains(where: { $0.messageId == message.messageId }) == false
else { return }
self?.messages.append(message)
if let self, self.messages.count > 100 {
self.messages.removeFirst(self.messages.count - 100)
}
}
})
listeners.append(sdk.onError { _ in
print("WuKongEasySDK operation failed")
})
self.sdk = sdk
do {
try await sdk.connect()
} catch {
stop() // Release the socket and listeners after timeout, auth, or network failure.
throw error
}
}
func sendText(to uid: String, text: String) async throws -> SendResult {
guard let sdk, isConnected else { throw WuKongError.notConnected }
let payload: MessagePayload = [
"type": 1,
"version": 1,
"content": text
]
return try await sdk.send(
channelId: uid,
channelType: .person,
payload: payload
)
}
func stop() {
if let sdk {
listeners.forEach { sdk.removeListener($0) }
sdk.disconnect()
}
listeners.removeAll()
messages.removeAll()
sdk = nil
isConnected = false
}
deinit {
// The view or application lifecycle should still call stop() explicitly.
}
}
```
Do not discard the anonymous closure passed to `onMessage`: `removeListener` needs the listener object returned during registration. The sample bounds connection and request handling at 15 seconds and calls `stop()` after failure; automatic reconnect is capped at 5 attempts, while the UI remains not ready. In SwiftUI, let a higher-level `@StateObject` own `EasyChatClient` and call `stop()` on logout or when the application-level owner exits.
## 4. Exchange the first message [#4-exchange-the-first-message]
`productAPI.fetchIMBootstrap()` is your application login API; `chatClient` is the `EasyChatClient` created above. This is an integration snippet; use the [official example](/en/sdk/easy/examples#ios) for a runnable app.
The device flag and payload shape are compatible with the current server. After product login, obtain Alice's `IMBootstrap` and start the client:
```swift
let aliceBootstrap = try await productAPI.fetchIMBootstrap()
try await chatClient.start(with: aliceBootstrap)
_ = try await chatClient.sendText(
to: "bob",
text: "Hello from iOS EasySDK"
)
print("SEND completed")
```
The returned `SendResult` means Alice received a server result for the send. Bob must independently observe the same product payload through `onMessage`; a local list insertion is not proof of peer receipt.
1. Sign in as Alice and Bob on two devices or in two independent app processes.
2. Enable send only after both sides observe `onConnect`.
3. Have Alice send to the person Channel `bob`, retaining `messageId` and `messageSeq`.
4. On Bob, verify `fromUid == "alice"`, the Channel, and the payload in `onMessage`.
5. Send from Bob to Alice and prove the reverse direction.
6. Leave the view or sign out, call `stop()`, and confirm that no duplicate listener or background connection remains.
## 5. Clean up [#5-clean-up]
Call `chatClient.stop()` when signing out or destroying the connection owner. It removes each `EventListener` and calls `disconnect()`. Reopening a page should not create another application connection. The example keeps only the latest 100 messages for display, not durable history.
## 6. Troubleshooting [#6-troubleshooting]
* **The compiler says the API requires iOS 15:** raise the deployment target to iOS 15; do not rely only on the lower platform declaration in `Package.swift`.
* **Connection or authentication fails:** reach the returned address from the device network, inspect WSS, certificates, and proxy Upgrade, then refresh the short-lived token. Never return a container-only address to the phone.
* **SEND or RECV cannot decode the payload:** confirm the server includes the EasySDK JSON-RPC compatibility implementation and the proxy does not rewrite messages. The pinned release sends objects; the server accepts object and Base64 input and emits object RECV.
* **The device category is unexpected:** ensure product code has not overridden `.app`, then compare APP `0`, WEB `1`, and PC `2`; do not carry forward an older literal.
* **Messages appear more than once:** register listeners only once and merge realtime and later synchronized results by `messageId`.
* **Alice gets a send result but Bob sees nothing:** inspect Alice's send result, Bob's realtime connection, and the product's later offline-sync path separately. Do not automatically resend a message that may already be committed.
## Alternative installation: CocoaPods [#alternative-installation-cocoapods]
```ruby
target 'YourApp' do
pod 'WuKongEasySDK', '1.1.1'
end
```
Run `pod install`, then open the `.xcworkspace`. Choose one installation method, not both.
## Next [#next]
Continue with [messaging](/en/guide/integration/messaging) and [production checks](/en/guide/integration/acceptance). For offline recovery, conversations, unread counts, or push, see [SDK selection](/en/sdk). [Versions and validation records](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md#ios) retain the exact environments and scope of past runs.
# Python quickstart (/en/sdk/easy/python/getting-started)
Use Python 3.11+, asyncio, and PyPI **0.1.0** for online messaging. Each client belongs to one event loop.
## 1. Prepare [#1-prepare]
Follow [authentication and Tokens](/en/guide/integration/authentication) so your trusted backend supplies each user's `uid`, `token`, and `websocketUrl`. Clients connect to the Gateway and do not call WuKongIM HTTP API management endpoints.
Device categories are APP `0`, WEB `1`, and PC/Desktop `2`. **Python defaults to Desktop `2`**; provision the Token for that same device category. The usual development address is `ws://127.0.0.1:5200`. Use `ws://127.0.0.1:5200/ws` only when the listener or proxy configures `/ws`. Use a reachable client address across machines and `wss://` in production.
To see messages first, follow [the official example](/en/sdk/easy/examples#python) with two clients. The steps below integrate the SDK into your application.
## 2. Install the SDK [#2-install-the-sdk]
This tutorial uses [PyPI `wukong-easy-sdk==0.1.0`](https://pypi.org/project/wukong-easy-sdk/0.1.0/) and requires Python 3.11+. The distribution name is `wukong-easy-sdk`; the import name is `wukong_easy_sdk`.
```sh
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --index-url https://pypi.org/simple "wukong-easy-sdk==0.1.0"
```
On Windows PowerShell, replace activation with `.venv\Scripts\Activate.ps1`. The runtime dependency is `websockets>=15.0.1,<18`; `uv.lock` pins development and validation dependencies.
## 3. Connect and listen [#3-connect-and-listen]
This example sends with the configured identity to `WKIM_PEER` and keeps receiving for 10 seconds. Bob must already be online. In a real application, keep the client alive until application shutdown.
```python
import asyncio
import os
from wukong_easy_sdk import AuthOptions, WKIM, WKIMChannelType, WKIMEvent
async def main():
im = WKIM.init(
os.environ.get("WKIM_URL", "ws://127.0.0.1:5200"),
AuthOptions(uid=os.environ["WKIM_UID"], token=os.environ["WKIM_TOKEN"]),
)
def receive(message):
# Pass message["payload"] to your application's UI or bounded queue.
# Never write complete messages or Tokens to production logs.
print("Message received")
listener = im.on(WKIMEvent.MESSAGE, receive)
im.on(WKIMEvent.ERROR, lambda error: print("EasySDK operation failed"))
async with im:
ack = await im.send(
os.environ["WKIM_PEER"], WKIMChannelType.PERSON,
{"type": 1, "content": "Hello from Python!"},
)
assert ack["reasonCode"] == 1
await asyncio.sleep(10)
im.off(WKIMEvent.MESSAGE, listener)
asyncio.run(main())
```
`async with im` waits for CONNECT authentication on entry and calls `destroy()` on exit. Use `WKIMChannelType.GROUP` for groups; your backend must establish the Channel and membership first. Payloads accept JSON objects or arrays, encoded as Base64 UTF-8 JSON. Incoming object, JSON-text, and Base64 JSON profiles are supported.
Python parameters use snake\_case, while received message and result dictionaries retain JS camelCase. SENDACK includes `messageId`, `messageSeq`, and `reasonCode`; received messages also contain `header`, `channelId`, `channelType`, `fromUid`, a seconds-based `timestamp`, and `payload`. Message IDs are strings and sequences retain full integer precision. `WKIMEvent.CUSTOM_EVENT` provides `id`, `type`, a milliseconds-based `timestamp`, and `data`.
`send()` accepts `client_msg_no`, `header`, `setting`, and `topic` keyword arguments. `header.redDot` defaults to true and preserves an explicit false. Optional message flags and Channel types still depend on server support.
Automatic RECVACK means the message entered the SDK dispatcher, not that application processing or reading completed. See [messaging](/en/guide/integration/messaging) for the distinction between send success, peer reception, and business processing.
## 4. Exchange the first message [#4-exchange-the-first-message]
Download the matching example source and keep using the same virtual environment:
```sh
git clone --branch v0.1.0 --depth 1 https://github.com/WuKongIM/WuKongEasySDK-Python.git
```
In two terminals, set each user's `WKIM_UID`, `WKIM_TOKEN`, `WKIM_PEER`, and optional `WKIM_URL`, then run:
```sh
python WuKongEasySDK-Python/examples/chat.py
```
For Alice, set `WKIM_UID=alice` and `WKIM_PEER=bob`; reverse these for Bob. Supply each Token through its trusted environment. Once both display `Connected`, send in both directions. Enter `/quit` to clean up. The example deliberately displays chat content; the SDK is silent by default.
Once both clients are connected, Alice sends to `bob` and Bob checks the sender and content in the message callback. Then Bob sends to `alice`. A send result means the server accepted the request; it is not recipient delivery or a read receipt.
## 5. Clean up [#5-clean-up]
Each instance belongs to one asyncio event loop; there is no global singleton. Sync and async callbacks run serially on a separate task. Async callbacks may `await im.send(...)` to reply, disconnect, or destroy the client. Do not block the event loop or wait for a subsequent event on the same serial dispatcher.
| Operation | Contract |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `await im.connect()` | Concurrent callers share authentication; connected calls return the current result |
| `im.is_connected` | The current connection is authenticated |
| `im.on(event, callback)` / `im.off(event, callback)` | Retain and remove the original callback; an executing callback may finish |
| `await im.ping()` | Require a matching response ID, including valid `result: null` |
| `await im.disconnect()` | Cancel pending requests, socket, heartbeat, and retries; reconnect remains possible |
| `await im.destroy()` | Permanently close and release listeners; idempotent |
| Changing account, Token, or URL | Close the old instance and create a new one |
Cancelling one `connect()` waiter does not cancel the shared connection; use `disconnect()` to stop it. Cancelled or timed-out sends release request capacity, but a message that reached the server may still commit.
## 6. Troubleshooting [#6-troubleshooting]
All `WKIMOptions` durations are seconds: 10 for the total connection deadline, 15 for requests, 25 between heartbeats, 10 for Pong, and 2 for close. An unexpected transport loss after authentication allows up to 5 retries, exponentially increasing from 1 second to a 30-second cap with 20% jitter. First-connect failure, authentication rejection, server-initiated disconnect, protocol errors, event saturation, certificate verification failure, and manual shutdown stop automatic retry.
Defaults allow 1,024 pending requests, 4 MiB of serialized pending request data, 1 MiB per wire message, and 256 queued events with a 4 MiB wire-size budget including the executing event; Python object overhead is additional. Request saturation returns `ErrorCode.QUEUE_FULL`. Event saturation closes the connection without acknowledging messages that could not enter the queue; lifecycle notifications are best effort under overload. Keep callbacks short and apply application backpressure.
There is no offline queue or automatic replay. A timeout or lost SENDACK can leave the commit outcome unknown. Retain `client_msg_no`, reconcile through your backend, then decide whether to retry. `WKIMError.code` retains the server reason or local `ErrorCode`; error text does not echo sensitive responses.
WSS verifies certificate chains and hostnames by default, with a TLS 1.2 minimum. Use `WKIMOptions(ca_file="/path/ca.pem")` for a private CA; the interactive example reads `WKIM_CA_FILE`. There is no verification bypass or automatic system-proxy discovery; supply the reachable Gateway/proxy URL directly.
Logging is off by default. `WKIMOptions(debug_logging=True)` enables only fixed lifecycle metadata, excluding Tokens, Payloads, URLs, raw frames, peer response text, and underlying exception objects.
## Optional: group messaging [#optional-group-messaging]
Use `WKIMChannelType.GROUP`; no client subscription is needed. A trusted backend creates the group and manages members and permissions; receive via `WKIMEvent.MESSAGE`. Cross-node membership updates require a server with the [membership cache fix](https://github.com/WuKongIM/WuKongIM/commit/2a295e0d9881ef5356728a85d56b052c4b0d9c86); upgrading the Python package cannot repair an older server. Online routes must recover after a Slot leader change, so continuous delivery during migration is not guaranteed. See the [group example](https://github.com/WuKongIM/WuKongEasySDK-Python/blob/527f37c876326e7ad3cc48c89828c4c3ffed09fc/examples/group_chat.py) for the optional application flow.
## Next [#next]
Continue with [messaging](/en/guide/integration/messaging) and [production checks](/en/guide/integration/acceptance). For offline recovery, conversations, unread counts, or push, see [SDK selection](/en/sdk). [Versions and validation records](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md#python) retain the exact environments and scope of past runs.
# Rust quickstart (/en/sdk/easy/rust/getting-started)
Use Rust 1.86+, Tokio, and crates.io **0.1.0** for online messaging in native applications. Browser/WASM targets are not supported.
## 1. Prepare [#1-prepare]
Your backend returns the current user's `uid`, short-lived `token` and `websocketUrl`. Protect registration and rotation as described in [authentication and tokens](/en/guide/integration/authentication). Clients must not call WuKongIM HTTP API management endpoints directly.
`Auth::new` defaults to **PC/Desktop `2`**. Wire values are APP `0`, WEB `1`, PC `2`; register the token under the same device category. If your native host uses APP, set `auth.device_flag = DeviceFlag::App` and match the backend registration.
`Auth::new` generates a `device_id` retained across that client's reconnects. Set your own persistent device ID when identity must survive process restarts. Create a `Client` per identity; `clone()` shares its connection across Tokio tasks.
To see messages first, follow [the official example](/en/sdk/easy/examples#rust) with two clients. The steps below integrate the SDK into your application.
## 2. Install the SDK [#2-install-the-sdk]
Official repository: [WuKongIM/WuKongEasySDK-Rust](https://github.com/WuKongIM/WuKongEasySDK-Rust). [Version `0.1.0` is available on crates.io](https://crates.io/crates/wukong-easy-sdk/0.1.0), requiring Rust **1.86+** and Tokio. Install the exact version below:
```toml
[dependencies]
wukong-easy-sdk = "=0.1.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde_json = "1"
```
The package name is `wukong-easy-sdk`; the Rust import name is `wukong_easy_sdk`. Commit your application's `Cargo.lock` to retain resolved dependencies. This implementation supports native TCP/TLS; WSS uses rustls and WebPKI roots. Browser/WASM is not supported.
## 3. Connect and listen [#3-connect-and-listen]
This complete program subscribes first, connects, sends one message to Bob and cleans up. Set the environment variables from your backend. Alice and Bob must have different UIDs, and Bob must already be online.
```rust
use serde_json::json;
use wukong_easy_sdk::{Auth, ChannelType, Client, Event, Options};
#[tokio::main]
async fn main() -> Result<(), Box> {
let client = Client::new(
std::env::var("WK_WS_URL")?,
Auth::new(std::env::var("WK_UID")?, std::env::var("WK_TOKEN")?),
Options::default(),
)?;
let mut events = client.subscribe();
let listener = tokio::spawn(async move {
loop {
match events.recv().await {
Ok(Event::Message(message)) => {
// Render/store message.payload in your UI; do not log its body.
let _ = &message.payload;
}
Ok(Event::CustomEvent(event)) => {
// Your application defines event.event_type and event.data.
let _ = (&event.event_type, &event.data);
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
// Events were lost; reconcile through your application backend.
break;
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
_ => {}
}
}
});
let result = async {
client.connect().await?;
let ack = client.send(
"bob", ChannelType::Person,
json!({"type": 1, "content": "Hello, Rust 🦀"}),
).await?;
// ack is server acceptance, not Bob reading or processing the message.
let _ = ack;
Ok::<_, wukong_easy_sdk::Error>(())
}.await;
client.destroy().await;
listener.abort();
let _ = listener.await;
result?;
Ok(())
}
```
A successful `send` returns `SendResult`, including string `message_id`, `u64` `message_seq` and `reason_code`. Business errors return `Error::Server { code }`, preserving the numeric code. SENDACK does not imply receipt, display or application completion; see [messaging](/en/guide/integration/messaging).
## 4. Exchange the first message [#4-exchange-the-first-message]
For ongoing bidirectional communication, use the repository's terminal example:
```bash
git clone https://github.com/WuKongIM/WuKongEasySDK-Rust.git
cd WuKongEasySDK-Rust
git checkout v0.1.0
cargo build --locked --example chat
```
From a trusted development terminal, follow [Run Official Examples](/en/sdk/easy/examples) to prepare credentials, setting both Alice and Bob's `device_flag` to `2`. Run these commands in separate terminals:
```bash
# Alice
WK_WS_URL=ws://127.0.0.1:5200 WK_UID=alice WK_TOKEN=alice-token \
WK_PEER_UID=bob cargo run --locked --example chat
# Bob
WK_WS_URL=ws://127.0.0.1:5200 WK_UID=bob WK_TOKEN=bob-token \
WK_PEER_UID=alice cargo run --locked --example chat
```
Wait for both connections before typing messages. The example reports server acceptance, receipt and connection state without logging message bodies. Your application can read `from_uid`, Channel and Payload from `Event::Message`. Enter `/quit` or use Ctrl-C to clean up.
## 5. Clean up [#5-clean-up]
| Task | Rust API and behavior |
| --------------------- | -------------------------------------------------------------------------------------- |
| Subscribe/unsubscribe | `subscribe()` returns a Tokio broadcast receiver; drop it to unsubscribe |
| Concurrent connection | `connect().await` joins the current attempt; cancelling one future does not cancel it |
| Initial failure | Returns an error; the application decides whether to retry |
| Network reconnect | An established connection's failure gets at most 5 exponential retries with jitter |
| Manual disconnect | `disconnect().await` cancels authentication, I/O and retries; later connect is allowed |
| Account exit | `destroy().await` permanently closes every clone; also stop subscriber tasks |
| Last handle dropped | Cancels the worker; explicitly await shutdown when completion matters |
Authentication rejection, server `disconnect` and manual shutdown do not reconnect. Defaults are a 25-second heartbeat interval, 10-second pong deadline, 5-second connection timeout, 15-second total SEND timeout and 5-second write timeout.
Queued and pending SENDs share a default limit of 256; excess calls return `Backpressure`. Events retain 256 entries; slow observers get `RecvError::Lagged`. The default maximum complete JSON-RPC message is 1 MiB including Base64 overhead. Adjust `Options` for your measured workload.
Automatic RECVACK confirms network receipt, not observer processing. Messages are acknowledged even without observers or when an observer falls behind. EasySDK has no durable inbox; reliable recovery requires application storage, deduplication and reconciliation. Do not ignore `Lagged`.
Sends are never replayed automatically. Timeout, cancellation or transport loss may leave acceptance unknown; preserve `SendOptions.client_msg_no` if your application retries under the server's idempotency contract. `SendOptions` also supports Header, Setting and Topic. SEND `red_dot` defaults to true and respects explicit false. Use `ChannelType::Group` for group messages after the backend prepares membership and permissions.
## 6. Troubleshooting [#6-troubleshooting]
**Can I resend after a timeout or disconnect?** The server may already have accepted the message. Keep the outcome unknown and reconcile using `SendOptions.client_msg_no`.
**Why does a listener receive `Lagged`?** Application consumption fell behind and events were lost. Bound the processing queue and reconcile through your backend; reconnect does not fetch history.
For private PKI, put DER root bytes in `Options.additional_root_certificates`.
This accepts at most 16 certificates of 64 KiB each, never private keys. Public
WebPKI roots remain trusted; hostname and expiry verification stay enabled.
```rust
let options = wukong_easy_sdk::Options {
additional_root_certificates: vec![std::fs::read("company-root.der")?],
..Default::default()
};
```
## Optional: group messaging [#optional-group-messaging]
The trusted backend creates the group and manages members through WuKongIM HTTP API.
After the backend confirms membership, use the group ID with `ChannelType::Group`:
```rust
let ack = client.send(
"project-team",
wukong_easy_sdk::ChannelType::Group,
serde_json::json!({"type": 1, "content": "Hello, team!"}),
).await?;
```
The receiving side continues to use `Event::Message`; check `channel_type`,
`channel_id`, `from_uid` and `payload`. SENDACK confirms server acceptance, not
that every member has processed the message. For a group that rejects strangers,
a nonmember or removed member receives `Error::Server { code: 3 }`; a denylisted
member receives code `4`. Report these permission failures to the application.
Membership management credentials belong on the trusted backend.
## Next [#next]
Continue with [messaging](/en/guide/integration/messaging) and [production checks](/en/guide/integration/acceptance). For offline recovery, conversations, unread counts, or push, see [SDK selection](/en/sdk). [Versions and validation records](https://github.com/WuKongIM/WuKongIM/blob/main/docs/superpowers/reports/2026-09-08-easysdk-validation-history.md#rust) retain the exact environments and scope of past runs.
# Flutter custom messages (/en/sdk/flutter/advanced/custom-messages)
When built-in payloads such as text and image cannot represent an order card or another application object, extend `WKMessageContent`.
## 1. Choose a content type [#1-choose-a-content-type]
Assign an integer to each application payload. Every client and your server must use the same value. Do not use the SDK-reserved range `1` through `99`.
```dart
const orderMessageType = 10001;
```
## 2. Define the payload [#2-define-the-payload]
```dart
class OrderMessageContent extends WKMessageContent {
String orderID;
String title;
OrderMessageContent({this.orderID = '', this.title = ''}) {
contentType = orderMessageType;
}
@override
Map encodeJson() => {
'order_id': orderID,
'title': title,
};
@override
WKMessageContent decodeJson(Map json) {
orderID = readString(json, 'order_id');
title = readString(json, 'title');
return this;
}
@override
String displayText() => '[Order] $title';
@override
String searchableWord() => title;
}
```
* `encodeJson` creates the data sent to the server.
* `decodeJson` restores an object from received JSON.
* `displayText` supplies the conversation preview.
* `searchableWord` supplies locally searchable text.
## 3. Register before reading messages [#3-register-before-reading-messages]
Register once after `setup`, before connecting or reading the local database:
```dart
await WKIM.shared.setup(options);
WKIM.shared.messageManager.registerMsgContent(
orderMessageType,
(data) => OrderMessageContent().decodeJson(
Map.from(data),
),
);
```
Without this registration, the SDK cannot restore the payload as `OrderMessageContent`. Keep registrations in one application startup service instead of repeating them in chat screens.
## 4. Send and render it [#4-send-and-render-it]
```dart
await WKIM.shared.messageManager.sendWithOption(
OrderMessageContent(orderID: 'A-1001', title: 'Awaiting payment'),
WKChannel('bob', WKChannelType.personal),
WKSendOptions(),
);
```
Render the received payload by its Dart type:
```dart
final content = message.messageContent;
if (content is OrderMessageContent) {
showOrderCard(content.orderID, content.title);
}
```
Keep future schema changes backward compatible: older clients should ignore new fields, and newer clients should provide defaults for missing fields.
# Flutter Advanced (/en/sdk/flutter/advanced)
Complete the [Quickstart](/en/sdk/flutter/quickstart), then add only the advanced capabilities your product needs. Unused providers do not need configuration.
## Before production [#before-production]
* `1.7.9` uses raw TCP, with no authenticated TLS switch in the public API. Use a controlled network entry or a transport design your security team has approved.
* `Options.debug` defaults to `true`. Set it to `false` and confirm Release logs do not expose tokens or message bodies.
* Local sqflite data has no SDK-level at-rest encryption. Combine platform file protection, backup rules, and sign-out cleanup for sensitive data.
* Several providers are one global slot. Install them through an application-scoped service so pages cannot replace each other.
* Test poor networks, foreground/background recovery, expired tokens, kicks, offline messages, account switching, and rollback on real devices.
# Flutter media and history (/en/sdk/flutter/advanced/media-and-history)
WuKongIMSDK manages media-message state and transport, but your application still chooses its object storage and history HTTP API. Both integrations use application-level providers.
## Upload media [#upload-media]
Install one upload provider during application startup:
```dart
WKIM.shared.messageManager.addOnUploadAttachmentListener(
(message, complete) async {
final content = message.messageContent;
if (content is! WKMediaMessageContent) {
complete(false, message);
return;
}
try {
content.url = await mediaUploader.upload(content.localPath);
complete(true, message);
} catch (_) {
complete(false, message);
}
},
);
```
Create and send an image payload from a local file:
```dart
final image = WKImageContent(1080, 1920)
..localPath = '/path/to/photo.jpg';
await WKIM.shared.messageManager.sendWithOption(
image,
WKChannel('team-1', WKChannelType.group),
WKSendOptions(),
);
```
On success, set the downloadable address on `content.url` before calling `complete(true, message)`. On failure, call `complete(false, message)` so the UI can show a failed state and a retry action.
## Fetch remote history [#fetch-remote-history]
When `getOrSyncHistoryMessages` cannot satisfy a request from the local database, the SDK invokes the history provider:
```dart
WKIM.shared.messageManager.addOnSyncChannelMsgListener(
(
channelID,
channelType,
startMessageSeq,
endMessageSeq,
limit,
pullMode,
complete,
) async {
try {
final response = await messageApi.loadHistory(
channelID: channelID,
channelType: channelType,
startMessageSeq: startMessageSeq,
endMessageSeq: endMessageSeq,
limit: limit,
pullMode: pullMode,
);
complete(toWKSyncChannelMsg(response));
} catch (_) {
complete(null);
}
},
);
```
`toWKSyncChannelMsg` converts your API response into `WKSyncChannelMsg`, including `startMessageSeq`, `endMessageSeq`, `more`, and `messages`. Each `WKSyncMsg` should preserve the server message ID, sequence, sender, channel, time, and payload.
Calling either setter again replaces the previous provider. Install them in one application-level IM service, and make sure every path invokes `complete`.
# HarmonyOS custom messages (/en/sdk/harmonyos/advanced/custom-messages)
When built-in payloads such as text and image cannot represent an order card or another application object, extend `WKMessageContent`.
## 1. Choose a content type [#1-choose-a-content-type]
Every client and your server must use the same integer for an application payload. Do not use the SDK-reserved range `1` through `99`.
```typescript
const ORDER_MESSAGE_TYPE: number = 10001
```
## 2. Define the payload [#2-define-the-payload]
```typescript
import { CommonUtil } from '@wukong/wkim/src/main/ets/common/CommonUtil'
import { WKMessageContent } from '@wukong/wkim/src/main/ets/model/WKMessageContent'
export class OrderMessageContent extends WKMessageContent {
orderId: string = ''
title: string = ''
constructor(orderId: string = '', title: string = '') {
super()
this.orderId = orderId
this.title = title
this.contentType = ORDER_MESSAGE_TYPE
}
encodeJson(): Record {
return {
'order_id': this.orderId,
'title': this.title
}
}
decodeJson(jsonStr: string): WKMessageContent {
const json = CommonUtil.jsonToRecord(jsonStr)
if (json !== undefined) {
this.orderId = CommonUtil.readString(json, 'order_id') ?? ''
this.title = CommonUtil.readString(json, 'title') ?? ''
}
return this
}
displayText(): string {
return `[Order] ${this.title}`
}
searchableWord(): string {
return this.title
}
}
```
* `encodeJson` creates the data sent to the server.
* `decodeJson` restores an object from the received JSON string.
* `displayText` supplies the conversation preview.
* `searchableWord` supplies locally searchable text.
## 3. Register before reading messages [#3-register-before-reading-messages]
Register once after `init`, before connecting or reading the local database:
```typescript
WKIM.shared.messageManager().registerMsgContent(
ORDER_MESSAGE_TYPE,
(jsonStr: string): WKMessageContent => {
return new OrderMessageContent().decodeJson(jsonStr)
}
)
```
Without registration, the SDK treats this as an unknown payload. Keep all registrations in one application startup service rather than repeating them in chat screens.
## 4. Send and render it [#4-send-and-render-it]
```typescript
WKIM.shared.messageManager().send(
new OrderMessageContent('A-1001', 'Awaiting payment'),
new WKChannel('bob', WKChannelType.personal)
)
```
Render the received payload by its ArkTS type:
```typescript
const content = message.messageContent
if (content instanceof OrderMessageContent) {
showOrderCard(content.orderId, content.title)
}
```
Keep future schema changes backward compatible: older clients should ignore new fields, and newer clients should provide defaults for missing fields.
# HarmonyOS advanced features (/en/sdk/harmonyos/advanced)
Complete the [Quickstart](/en/sdk/harmonyos/quickstart), then add only the advanced capabilities your product needs. Unused providers do not need configuration.
## Before production [#before-production]
* The persistent connection currently uses raw TCP and exposes no authenticated TLS switch. Use a controlled network entry or a transport design that has passed your security review.
* `WKConfig.debug` does not disable the current SDK logs. Inspect release logs and prevent tokens, message payloads, and user identifiers from being emitted.
* The local `RelationalStore` does not enable SDK-level at-rest encryption. Protect sensitive data with platform storage controls, backup policy, and logout cleanup.
* `WKIM.shared`, its managers, and providers are process-global state. Install them in one application service instead of letting screens replace one another's configuration.
* There is no single reset API for every provider and callback. Call `disConnection(true)` before changing UID and verify isolation; restart the process before initializing another account when isolation is uncertain.
* Test weak networks, foreground/background recovery, token expiry, kick-out, offline messages, account switching, and rollback on real devices.
# HarmonyOS media and history (/en/sdk/harmonyos/advanced/media-and-history)
The SDK manages media-message state and transport, but your application still chooses its object storage and history HTTP API. Both integrations use `WKIM.shared.config.provider`.
## Upload media [#upload-media]
Install one upload provider during application startup:
```typescript
WKIM.shared.config.provider.uploadAttachmentCallback = async (
message: WKMsg
): Promise<[boolean, WKMsg]> => {
const content = message.messageContent
if (!(content instanceof WKImageContent)) {
return [false, message]
}
try {
content.url = await mediaUploader.upload(content.localPath)
message.messageContent = content
return [true, message]
} catch (_) {
return [false, message]
}
}
```
Create an image payload from a local file:
```typescript
const image = new WKImageContent('/data/storage/el2/base/files/photo.jpg')
image.width = 1080
image.height = 1920
WKIM.shared.messageManager().send(
image,
new WKChannel('team-1', WKChannelType.group)
)
```
Set the downloadable address on `content.url` before returning success. Returning `[false, message]` puts the message into a failed state so the UI can offer retry.
The SDK already registers image payloads. Voice and video models require `registerMsgContent` first, followed by type-specific handling in the same upload provider.
## Fetch remote history [#fetch-remote-history]
Set the history provider and convert your application API response to `WKSyncChannelMsg`:
```typescript
WKIM.shared.config.provider.syncMessageCallback = async (
channel: WKChannel,
options: SyncOptions
): Promise => {
const response = await messageApi.sync({
channelId: channel.channelId,
channelType: channel.channelType,
startMessageSeq: options.startMessageSeq,
endMessageSeq: options.endMessageSeq,
limit: options.limit,
pullMode: options.pullMode
})
return toWKSyncChannelMsg(response)
}
```
The converted result contains `startMessageSeq`, `endMessageSeq`, `more`, and `msgs`. Each `WKMsg` should preserve the server message ID, sequence, sender, channel, time, and payload JSON.
The chat screen continues to call `getOrSyncHistoryMessages`. The SDK reads local data first, calls this provider when needed, and then stores the returned messages locally.
Assigning the field again replaces its previous implementation. Set providers in one application-level IM service, and make sure each Promise settles on both success and failure.
# iOS Custom Messages (/en/sdk/ios/advanced/custom-messages)
Content beyond text and images, such as an order card or shared location, can be a `WKMessageContent` subclass. Every client must use the same type number and JSON fields for the same content.
## 1. Define the content [#1-define-the-content]
This example uses an order card. `10001` is only an example; maintain a shared registry of type numbers so product messages do not collide.
```objc
@interface OrderMessageContent : WKMessageContent
@property (nonatomic, copy) NSString *orderID;
@property (nonatomic, copy) NSString *title;
@end
@implementation OrderMessageContent
+ (NSNumber *)contentType {
return @(10001);
}
- (NSDictionary *)encodeWithJSON {
return @{
@"order_id": self.orderID ?: @"",
@"title": self.title ?: @""
};
}
- (void)decodeWithJSON:(NSDictionary *)json {
self.orderID = [json[@"order_id"] isKindOfClass:[NSString class]]
? json[@"order_id"] : @"";
self.title = [json[@"title"] isKindOfClass:[NSString class]]
? json[@"title"] : @"";
}
- (NSString *)conversationDigest {
return [NSString stringWithFormat:@"[Order] %@", self.title];
}
- (NSString *)searchableWord {
return self.title;
}
@end
```
Validate types and missing fields while decoding network data. Clients may not all run the same application version.
## 2. Register before reading messages [#2-register-before-reading-messages]
```objc
[[WKSDK shared] registerMessageContent:[OrderMessageContent class]];
```
Register once while configuring the SDK at app startup, before connecting or reading local messages. Unregistered content cannot be restored as your product type.
## 3. Send and render [#3-send-and-render]
```objc
OrderMessageContent *content = [OrderMessageContent new];
content.orderID = @"order-2026-001";
content.title = @"Coffee beans";
WKChannel *channel = [WKChannel personWithChannelID:@"bob"];
[[WKSDK shared].chatManager sendMessage:content channel:channel];
```
Render known types and show a safe placeholder for unknown content:
```objc
if ([message.content isKindOfClass:[OrderMessageContent class]]) {
OrderMessageContent *order = (OrderMessageContent *)message.content;
[self showOrderWithID:order.orderID title:order.title];
} else {
[self showUnsupportedMessage];
}
```
Keep schema changes backward compatible: give new fields defaults, do not reuse an old field for a new meaning, and never change a published type number.
# iOS Advanced (/en/sdk/ios/advanced)
Complete the [Quickstart](/en/sdk/ios/quickstart), then add only the advanced capabilities your product needs. Unused providers do not need configuration.
## Before production [#before-production]
* The default connection in `1.1.1` is raw TCP, with no authenticated TLS switch in the public configuration. Use a controlled network entry or move to a transport design your security team has approved.
* Set `options.isDebug = NO` and confirm Release logs do not expose tokens or message bodies.
* The local message database is separated by UID, but a UID is not an encryption key. Design iOS file protection, backups, and sign-out cleanup together.
* Test poor networks, background recovery, expired tokens, kicks, offline messages, and rollback on real devices.
# iOS Media & History (/en/sdk/ios/advanced/media-and-history)
An image message combines a local file with a remote location available to other devices. The SDK owns message states and task scheduling; your app performs object-storage upload, download, and authorization.
## Send an image [#send-an-image]
Create image content and send it like text:
```objc
WKImageContent *content = [WKImageContent initWithImage:image];
WKChannel *channel = [WKChannel groupWithChannelID:@"team-1"];
WKMessage *message = [[WKSDK shared].chatManager sendMessage:content
channel:channel];
```
Configure an upload-task factory before sending images:
```objc
[WKSDK shared].mediaManager.uploadTaskProvider = ^id(WKMessage *message) {
return [[AppMediaUploadTask alloc] initWithMessage:message
storage:objectStorage];
};
```
`AppMediaUploadTask` is an application type, not an SDK type. It implements `WKTaskProto`, uploads the file to your storage service, writes the remote location into the media content, and reports success or failure. Upload credentials should be short lived and limited by path, size, and file type.
## Download media [#download-media]
Provide a download task, then ask the manager to run it:
```objc
[WKSDK shared].mediaManager.downloadTaskProvider = ^id(WKMessage *message) {
return [[AppMediaDownloadTask alloc] initWithMessage:message
storage:objectStorage];
};
[[WKSDK shared].mediaManager download:message
callback:^(WKMediaDownloadState state,
CGFloat progress,
NSError *error) {
[self updateDownloadProgress:progress state:state];
}];
```
Validate size and type before caching files, and remove private media according to your sign-out policy.
## Fill remote message history [#fill-remote-message-history]
`pullLastMessages:` reads the local database first. Connect `syncChannelMessageProvider` to your history API when the requested local range is incomplete:
```objc
[WKSDK shared].chatManager.syncChannelMessageProvider = ^(
WKChannel *channel,
uint32_t startMessageSeq,
uint32_t endMessageSeq,
NSInteger limit,
WKPullMode pullMode,
WKSyncChannelMessageCallback callback
) {
[historyAPI fetchChannel:channel
start:startMessageSeq
end:endMessageSeq
limit:limit
mode:pullMode
completion:callback];
};
```
The application API returns a `WKSyncChannelMessageModel`. Always invoke `callback`; on failure, pass an error so the caller can stop loading and offer retry.
## Common problems [#common-problems]
* Image remains uploading: ensure the task eventually reports success or failure and writes the remote location after upload.
* Other devices cannot display the image: never use a sandbox-local path as a remote URL.
* Duplicate history: merge by channel and `messageSeq`, not array position.
* Pagination never finishes: the server must express the end of data and the provider must complete its callback.
# JavaScript / Web Custom Messages (/en/sdk/javascript/advanced/custom-messages)
Content beyond text and images, such as an order card or shared location, can extend `MessageContent`. Every client must use the same type number and JSON fields for the same content.
## 1. Define the content [#1-define-the-content]
`10001` is only an example; maintain a shared registry of type numbers so product messages do not collide.
```typescript
class OrderMessageContent extends MessageContent {
orderID = ''
title = ''
constructor(orderID = '', title = '') {
super()
this.orderID = orderID
this.title = title
}
get contentType(): number {
return 10001
}
get conversationDigest(): string {
return `[Order] ${this.title}`
}
encodeJSON() {
return {
order_id: this.orderID,
title: this.title,
}
}
decodeJSON(value: Record) {
this.orderID = typeof value.order_id === 'string' ? value.order_id : ''
this.title = typeof value.title === 'string' ? value.title : ''
}
}
```
Validate types and missing fields while decoding because users can run different application versions.
## 2. Register before connecting [#2-register-before-connecting]
```typescript
const ORDER_CONTENT_TYPE = 10001
sdk.register(ORDER_CONTENT_TYPE, () => new OrderMessageContent())
```
Register once in each page context. Unregistered content is decoded as an unknown type.
## 3. Send and render [#3-send-and-render]
```typescript
await sdk.chatManager.send(
new OrderMessageContent('order-2026-001', 'Coffee beans'),
new Channel('bob', ChannelTypePerson),
)
```
Inspect the runtime type when receiving:
```typescript
if (message.content instanceof OrderMessageContent) {
showOrder(message.content.orderID, message.content.title)
} else {
showUnsupportedMessage()
}
```
Give new fields defaults, do not reuse old fields for new meanings, and never change a published type number. Field names and numbers must match exactly across iOS, Android, Flutter, and HarmonyOS.
# JavaScript / Web Advanced (/en/sdk/javascript/advanced)
Complete the [Quickstart](/en/sdk/javascript/quickstart), then add only the advanced capabilities your product needs.
## Before production [#before-production]
* Use `wss://` and verify certificates, reverse-proxy WebSocket Upgrade, and CSP.
* Some console output in `1.3.5` is not controlled by `config.debug` and can contain decoded message data. Apply an approved removal or redaction approach and inspect the final bundle.
* Keep tokens in memory or controlled session storage; exclude them from URLs, analytics, error reporting, and browser logs.
* Test network loss, sleeping tabs, expired tokens, kicks, offline deduplication, account switching, and rollback.
# JavaScript Offline Recovery & UniApp Migration (/en/sdk/javascript/advanced/offline-and-uniapp)
The JavaScript SDK has no built-in persistent message database. Your application server supplies data after page reload, during disconnected periods, and for history pagination.
## Offline recovery [#offline-recovery]
First implement the message synchronization provider:
```typescript
sdk.config.provider.syncMessagesCallback = async (channel, options) => {
const rows = await messageApi.sync({
channelID: channel.channelID,
channelType: channel.channelType,
startMessageSeq: options.startMessageSeq,
endMessageSeq: options.endMessageSeq,
limit: Math.min(options.limit, 100),
pullMode: options.pullMode,
})
return rows.map(toSDKMessage)
}
```
After reconnecting, continue from the last saved message sequence and deduplicate by `messageID` or by `messageSeq` within the channel:
```typescript
const options = new SyncOptions()
options.startMessageSeq = lastMessageSeq
options.limit = 50
options.pullMode = PullMode.Up
const recovered = await sdk.chatManager.syncMessages(channel, options)
mergeMessages(recovered)
```
Keep the cursor when synchronization fails and allow retry. Do not advance it before messages are processed successfully.
## Migrate from the old UniApp SDK [#migrate-from-the-old-uniapp-sdk]
The official `wukongimuniappsdk` is no longer maintained. Do not add it to new projects. Migrate existing projects in this order:
```bash
npm uninstall wukongimuniappsdk
npm install --save-exact wukongimjssdk@1.3.5
```
Import from the package root, never from `wukongimjssdk/lib/*`:
```typescript
import WKSDK, {
Channel,
ConnectStatus,
MessageText,
} from 'wukongimjssdk'
```
Then migrate each integration point:
1. Set `uid`, `token`, WebSocket endpoint, and device type.
2. Reconnect message, conversation, and channel providers.
3. Register custom content before connecting.
4. Use `addConnectStatusListener`, `addMessageListener`, and `addMessageStatusListener`.
5. Send through `chatManager.send(...)`.
6. Remove the same function references when components unmount.
### UniApp runtime notes [#uniapp-runtime-notes]
Version `1.3.5` automatically selects global `uni.connectSocket`, `wx.connectSocket`, or native `WebSocket`. Do not force the choice through `config.platform`; in the current implementation that value does not initialize the socket factory, which can mix one creation path with another event API.
Set device type by target: a native app container is usually `0`, H5/Web is `1`, and desktop is `2`. Choose a mini-program type from your product protocol rather than assuming based only on JavaScript.
App, H5, and every mini-program are different runtimes. Test WSS allowlists, certificates, foreground/background behavior, network switching, live messages, recovery, and deduplication separately. Success in one target does not establish success in another.