Server API, client SDK, REST endpoints, and database schema for the LumenJS Communication module.
The communication module exports two main server-side entry points:
createCommunicationHandler(options?) -- returns a LumenJS socket handler function. Registers all chat, call, signaling, and encryption event listeners on each socket connection.
import { createCommunicationHandler } from '@nuraly/lumenjs/dist/communication/server.js'; export const socket = createCommunicationHandler({ db: useDb(), // optional, persist messages to SQLite getUserId(headers, query) { // optional, custom user ID extraction return headers['x-user-id'] || query.userId; }, config: { ... }, // optional, CommunicationConfig overrides });
createCommunicationApiHandlers(db) -- returns an object with REST-friendly methods for querying conversations and messages.
| Method | Signature | Description |
|---|---|---|
getConversations | (userId, { limit?, offset? }) => any[] | List conversations for a user with unread counts |
getMessages | (conversationId, { limit?, before? }) => any[] | Paginated message history (cursor-based) |
createConversation | ({ type, name?, participantIds }) => any | Create a new direct or group conversation |
searchMessages | (query, { conversationId?, limit? }) => any[] | Full-text search across messages |
getMessage | (messageId) => any | Fetch a single message by ID |
deleteMessage | (messageId, userId) => RunResult | Soft-delete a message (only the sender can delete) |
Additional server exports:
| Export | Description |
|---|---|
useCommunicationStore() | Returns the singleton in-memory store (presence, typing, calls, keys) |
ensureCommunicationTables(db) | Creates all required DB tables if they don't exist |
All client communication is performed through this.emit(event, data) on page components with a socket() export. Server responses arrive as individual properties (each key from push() is spread onto the component).
Client-to-server events:
| Event | Payload | Description |
|---|---|---|
message:send | { conversationId, content, type, replyTo?, attachment? } | Send a chat message |
message:read | { conversationId, messageId } | Mark a message as read |
typing:start | { conversationId } | Signal that the user started typing |
typing:stop | { conversationId } | Signal that the user stopped typing |
presence:update | { status } | Update presence ('online', 'away', 'busy') |
conversation:join | { conversationId } | Join a conversation room to receive its events |
conversation:leave | { conversationId } | Leave a conversation room |
call:initiate | { conversationId, type, calleeIds } | Start an audio or video call |
call:respond | { callId, action } | Accept or reject an incoming call |
call:hangup | { callId, reason } | End a call |
call:media-toggle | { callId, audio?, video?, screenShare? } | Toggle media tracks during a call |
signal:offer | SignalOffer | Send WebRTC SDP offer |
signal:answer | SignalOffer | Send WebRTC SDP answer |
signal:ice-candidate | SignalIceCandidate | Send ICE candidate |
encryption:upload-keys | KeyBundle | Upload public key bundle to server |
encryption:request-keys | { recipientId } | Request a user's public key bundle |
encryption:session-init | { recipientId, sessionId, envelope } | Relay encrypted session initialization |
Server-to-client events (spread as event + data properties):
| Event | Payload | Description |
|---|---|---|
message:new | Message | New message in a joined conversation |
message:updated | Message | Message was edited or deleted |
message:status | { messageId, status } | Message delivery status changed |
typing:update | TypingIndicator | User started/stopped typing |
presence:changed | PresenceUpdate | User's presence status changed |
conversation:updated | Conversation | Conversation metadata changed |
read-receipt:update | { conversationId, messageId, readBy } | A user read a message |
call:incoming | Call | Incoming call notification |
call:state-changed | { callId, state, endReason? } | Call state transitioned |
call:participant-joined | { callId, participant } | User joined the call |
call:participant-left | { callId, userId } | User left the call |
call:media-changed | { callId, userId, audio?, video?, screenShare? } | Participant toggled media |
signal:offer | SignalOffer | Incoming WebRTC SDP offer |
signal:answer | SignalOffer | Incoming WebRTC SDP answer |
signal:ice-candidate | SignalIceCandidate | Incoming ICE candidate |
encryption:keys-response | KeyExchangeResponse | Requested key bundle response |
encryption:session-init | { senderId, sessionId, envelope } | Incoming encrypted session init |
encryption:session-established | { sessionId, senderId } | Session successfully established |
encryption:keys-depleted | { userId } | One-time pre-keys exhausted, upload more |
When you wire up createCommunicationApiHandlers() in your API routes, the recommended endpoint structure uses the /__nk_comm/ prefix:
| Method | Route | Handler | Description |
|---|---|---|---|
GET | /__nk_comm/conversations | getConversations(userId, { limit, offset }) | List user's conversations with unread counts |
POST | /__nk_comm/conversations | createConversation({ type, name, participantIds }) | Create a new conversation |
GET | /__nk_comm/conversations/:id/messages | getMessages(id, { limit, before }) | Paginated message history |
GET | /__nk_comm/messages/:id | getMessage(id) | Get a single message |
DELETE | /__nk_comm/messages/:id | deleteMessage(id, userId) | Soft-delete a message |
GET | /__nk_comm/messages/search | searchMessages(q, { conversationId, limit }) | Search messages by content |
Example API route file:
// api/__nk_comm/conversations.ts import { createCommunicationApiHandlers } from '@nuraly/lumenjs/dist/communication/server.js'; import { useDb } from '@nuraly/lumenjs/db'; import { ensureCommunicationTables } from '@nuraly/lumenjs/dist/communication/schema.js'; const db = useDb(); ensureCommunicationTables(db); const comm = createCommunicationApiHandlers(db); export function GET(req) { const userId = req.headers['x-user-id']; if (!userId) return { error: 'Unauthorized', status: 401 }; return comm.getConversations(userId, { limit: Number(req.query.limit) || 50, offset: Number(req.query.offset) || 0, }); } export function POST(req) { return comm.createConversation(req.body); }
Call ensureCommunicationTables(db) to auto-create all required tables. The schema uses SQLite with foreign keys and cascading deletes.
conversations
| Column | Type | Constraints |
|---|---|---|
id | TEXT | PRIMARY KEY |
type | TEXT | NOT NULL CHECK(type IN ('direct', 'group')) |
name | TEXT | Nullable (optional for group chats) |
created_at | TEXT | NOT NULL DEFAULT datetime('now') |
updated_at | TEXT | NOT NULL DEFAULT datetime('now') |
conversation_participants
| Column | Type | Constraints |
|---|---|---|
conversation_id | TEXT | NOT NULL, FK → conversations(id) CASCADE |
user_id | TEXT | NOT NULL |
role | TEXT | NOT NULL DEFAULT 'member' |
joined_at | TEXT | NOT NULL DEFAULT datetime('now') |
Primary key: (conversation_id, user_id)
messages
| Column | Type | Constraints |
|---|---|---|
id | INTEGER | PRIMARY KEY AUTOINCREMENT |
conversation_id | TEXT | NOT NULL, FK → conversations(id) CASCADE |
sender_id | TEXT | NOT NULL |
content | TEXT | NOT NULL DEFAULT '' |
type | TEXT | NOT NULL DEFAULT 'text' |
reply_to | TEXT | Nullable (message ID being replied to) |
attachment | TEXT | Nullable (JSON-serialized MessageAttachment) |
status | TEXT | NOT NULL DEFAULT 'sent' |
encrypted | INTEGER | NOT NULL DEFAULT 0 (boolean flag) |
created_at | TEXT | NOT NULL DEFAULT datetime('now') |
updated_at | TEXT | Nullable |
read_receipts
| Column | Type | Constraints |
|---|---|---|
message_id | INTEGER | NOT NULL, FK → messages(id) CASCADE |
user_id | TEXT | NOT NULL |
read_at | TEXT | NOT NULL DEFAULT datetime('now') |
Primary key: (message_id, user_id)
encryption_keys
| Column | Type | Constraints |
|---|---|---|
user_id | TEXT | PRIMARY KEY |
identity_key | TEXT | NOT NULL (Base64 public key) |
signed_pre_key_id | INTEGER | NOT NULL |
signed_pre_key | TEXT | NOT NULL (Base64 public key) |
signed_pre_key_signature | TEXT | NOT NULL (Base64 signature) |
uploaded_at | TEXT | NOT NULL DEFAULT datetime('now') |
encryption_prekeys
| Column | Type | Constraints |
|---|---|---|
user_id | TEXT | NOT NULL, FK → encryption_keys(user_id) CASCADE |
key_id | INTEGER | NOT NULL |
public_key | TEXT | NOT NULL (Base64) |
Primary key: (user_id, key_id)
Indexes:
idx_messages_conversation on messages(conversation_id, created_at) -- fast message history queriesidx_participants_user on conversation_participants(user_id) -- fast conversation lookups by userensureCommunicationTables(db) once at startup. All tables use CREATE TABLE IF NOT EXISTS, so calling it multiple times is safe. The schema uses ON DELETE CASCADE, deleting a conversation removes its messages, participants, and read receipts automatically.