SDK Documentation
TypeScript/JavaScript SDK for Yappa RT — a production-grade, multi-tenant WebSocket messaging engine.
Introduction
Yappa SDK is the official client for Yappa RT, a horizontally scalable WebSocket server built with Rust (Axum/Tokio). It provides real-time messaging with complete tenant isolation, JWT authentication, and cross-node message delivery via Redis pub/sub.
Key Features
- Auto-reconnect with exponential backoff
- Message deduplication via message_id
- Heartbeat monitoring for dead connection detection
- Send queue buffers messages while offline
- Multi-device support (same user, multiple connections)
System Architecture
Messages flow through multiple layers to ensure delivery across nodes:
Components
| Component | Tech | Purpose |
|---|---|---|
| yappa-rt | Rust (Axum/Tokio) | WebSocket server, message routing, tenant limits |
| yappa-auth | Node.js (Express) | User authentication, JWT issuance |
| yappa-sdk | TypeScript | Browser/Node.js client SDK |
| Redis | Redis 7 | Pub/sub for cross-node messaging, tenant limits |
| Kafka | Kafka 3.7 | Durable message streaming (optional) |
| PostgreSQL | Postgres 16 | User storage, message persistence |
Installation
Install the SDK via your preferred package manager:
# Using npm
npm install @yappa-rs/yappa-sdk
# Using yarn
yarn add @yappa-rs/yappa-sdk
# Using pnpm
pnpm add @yappa-rs/yappa-sdkPeer Dependencies (Node.js only)
npm install wsWebSocket API. No polyfills needed.Quick Start
import { RealtimeClient } from '@yappa-rs/yappa-sdk';
// 1. Initialize with your JWT token
const client = new RealtimeClient({
url: 'wss://your-server.com/ws',
token: 'eyJhbGciOiJIUzI1NiIs...',
authMode: 'query' // Required for browsers
});
// 2. Listen for messages
client.on('message', (msg) => {
console.log(msg.channel_type + ': ' + msg.sender_id + ' -> ' + msg.payload.text);
});
// 3. Connect
await client.connect();
// 4. Send a direct message
client.sendDM('bob_123', 'Hello Bob!');Connection Flow
When you call client.connect(), the following sequence occurs:
/ws endpoint with JWT in the Authorization header or query string.tenant_id and user_id from claims.MAX_USERS_PER_TENANT via a Redis Lua script.ConnectionRegistry, keyed by (tenant_id, user_id, connection_id).connected event and drains any queued messages.Connection Identity
Every WebSocket connection is uniquely identified by the tuple (tenant_id, user_id):
- Identity is extracted from the JWT and is immutable for the socket’s lifetime
- One user can have multiple concurrent connections (multi-device)
- Each connection gets a unique
connection_id(UUID v4)
Connection States
| State | Description |
|---|---|
disconnected | No active connection |
connecting | WebSocket upgrade in progress, awaiting auth |
connected | Connection active, ready to send/receive |
reconnecting | Connection lost, SDK attempting reconnect |
error event.Direct Messages
Send a 1-to-1 message using the sendDM method:
client.sendDM('bob_123', 'Hello Bob!');What Happens on the Server
{"channel_type":"DM","user_id":"bob_123","content":"Hello Bob!"}message_id (UUID), conversation_id (SHA-256 hash of sorted user IDs), and a timestamp.user:{tenant_id}:{recipient_user_id} channel.Server Response Format
{
"type": "chat",
"message_id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "tenant_abc",
"channel_type": "DM",
"channel_id": "bob_123",
"sender_id": "alice",
"timestamp": 1700000000,
"conversation_id": "uuid-derived-from-sha256",
"payload": {
"text": "Hello Bob!",
"meta": {}
}
}Conversation ID Generation
The conversation_id is deterministically generated from participant IDs:
// Server pseudocode
participants = [sender_id, recipient_id].sort()
combined = "alice:bob"
hash = SHA256(combined)
conversation_id = UUID.from_bytes(hash[0:16])Groups
Group operations allow creating, joining, leaving, and sending messages to channels. Groups are scoped to a tenant and persisted to PostgreSQL for durability.
createGroup(groupId)
Creates a new group and automatically joins the creator.
client.createGroup('team-alpha');joinGroup(groupId)
Subscribes the current user to a group’s message stream. The group must already exist.
client.joinGroup('team-alpha');sendGroupMessage(groupId, content)
client.sendGroupMessage('team-alpha', 'Hi team!');"You must join the group before sending messages"leaveGroup(groupId) / deleteGroup(groupId)
client.leaveGroup('team-alpha');
client.deleteGroup('team-alpha');leaveGroup— removes you from the in-memory member set; no database change.deleteGroup— removes the group from the in-memory registry; database records persist for history.
group_members table has a foreign key to groups(conversation_id), so when a user joins, the server first looks up the group by tenant_id + name to resolve the conversation_id before inserting the membership row.Configuration Options
Pass these options when initializing RealtimeClient:
| Option | Type | Default | Description |
|---|---|---|---|
url required | string | — | WebSocket endpoint (ws:// or wss://) |
token required | string | — | HS256 JWT with tenant_id and user_id claims |
authMode | 'header' | 'query' | 'header' | Use 'query' for browsers (no custom header support) |
heartbeatTimeout | number | 35000 | Ms without activity before assuming the connection is dead |
reconnect | boolean | true | Auto-reconnect on disconnect |
maxReconnectAttempts | number | Infinity | Max reconnection tries before reconnect_failed |
reconnectBaseDelay | number | 1000 | Initial delay (ms), doubles each attempt |
reconnectMaxDelay | number | 30000 | Maximum reconnection delay cap |
dedup | boolean | true | Deduplicate messages by message_id |
dedupTTL | number | 60000 | How long to remember seen message IDs (ms) |
maxQueueSize | number | 1000 | Max messages buffered while offline |
logLevel | 'debug' | 'info' | 'warn' | 'error' | 'silent' | 'warn' | Console logging verbosity |
refreshUrl | string | — | URL to POST for automatic token refresh |
Authentication
JWT Requirements
Your JWT must be signed with HS256 and contain the following claims:
| Claim | Required | Description |
|---|---|---|
tenant_id | required | Organization/tenant identifier |
user_id | required | User identifier within the tenant |
exp | required | Expiration timestamp |
iss | optional | Issuer (validated if present) |
aud | optional | Audience (validated if present) |
Auth Modes
Header Mode Default
const client = new RealtimeClient({
url: 'wss://server.com/ws',
token: 'eyJ...',
authMode: 'header' // Sends: Authorization: Bearer eyJ...
});Query Mode Recommended for Browsers
const client = new RealtimeClient({
url: 'wss://server.com/ws',
token: 'eyJ...',
authMode: 'query' // Sends: wss://server.com/ws?token=eyJ...
});Token Refresh
Configure automatic token refresh — the SDK refreshes every 4 minutes:
const client = new RealtimeClient({
url: 'wss://server.com/ws',
token: 'eyJ...',
refreshUrl: 'https://auth.server.com/api/refresh'
});The SDK POSTs to refreshUrl with credentials (cookies), expecting {"access_token": "..."} in the response.
Reconnection Strategy
The SDK automatically reconnects with exponential backoff when the connection drops, with jitter to prevent thundering herds. The maximum backoff is capped at 30 seconds.
Server-Side: Connection Cleanup
When a connection drops, the server’s ConnectionGuard (Drop trait) ensures:
- Connection is removed from
ConnectionRegistry - The user’s slot is released via
TenantLimiter.release() - Group memberships are preserved and reattached on reconnect
SDK-Side: Reconnection Flow
client.on('reconnecting', (attempt) => {
console.log('Reconnect attempt ' + attempt);
});
client.on('reconnected', () => {
console.log('Back online!');
// Queued messages are automatically flushed
});reconnecting event with the attempt numbermin(baseDelay * 2^(attempt-1), maxDelay) — default: 1s → 2s → 4s → … → 30sreconnected, drains the queue, rejoins groupsMessage Queue
Messages sent while offline are queued (up to maxQueueSize) and flushed on reconnect:
// These are queued if not connected
client.sendDM('bob', 'Message 1');
client.sendDM('bob', 'Message 2');
// On reconnect: both are sent in orderRealtimeClient
The primary class for interacting with the Yappa realtime server. It extends standard EventTarget/EventEmitter paradigms.
Methods
| Method | Parameters | Description |
|---|---|---|
connect() | — | Establishes the WebSocket connection. Returns a Promise. |
disconnect() | — | Gracefully closes the connection and cleans up listeners. |
sendDM(userId, content) | userId: string, content: string | Sends a direct message (1–64,000 chars). |
sendGroupMessage(groupId, content) | groupId: string, content: string | Sends a group message (must join first). |
joinGroup(groupId) | groupId: string | Joins a group to receive its messages. |
leaveGroup(groupId) | groupId: string | Leaves a group. |
createGroup(groupId) | groupId: string | Creates and auto-joins a new group. |
deleteGroup(groupId) | groupId: string | Deletes a group. |
updateToken(token) | token: string | Updates the authentication token on an existing client. |
on(event, handler) | event: string, handler: Function | Subscribes to an event. Returns an unsubscribe function. |
Properties
| Property | Type | Description |
|---|---|---|
state | ConnectionState | Current connection state (read-only) |
Events
Listen to lifecycle and message events using client.on(event, handler).
| Event | Payload | When Fired |
|---|---|---|
connected | void | WebSocket connection established and registered |
disconnected | reason: string | Connection closed (e.g. "manual", "transport closed") |
reconnecting | attempt: number | Starting a reconnection attempt |
reconnected | void | Successfully reconnected |
reconnect_failed | void | Max reconnect attempts exhausted |
message | ServerMessage | Any incoming chat message or group lifecycle event |
dm | ServerMessage | Direct message (channel_type: "DM") |
group_message | ServerMessage | Group message (channel_type: "GROUP") |
group_join | ServerMessage | User joined a group (type: "group_join") |
error | RealtimeError | Socket, auth, or connection error |
Usage
const unsubscribe = client.on('message', (msg) => {
console.log(msg);
});
// Later: stop listening
unsubscribe();Type Definitions
ServerMessage
interface ServerMessage {
type: string; // "chat" or "group_join"
message_id: string; // UUID v4
tenant_id: string;
channel_type: ChannelType;
channel_id: string; // recipient user_id or group_id
sender_id: string;
timestamp: number; // Unix seconds
conversation_id: string; // UUID (DM: derived from participants, Group: group_id)
payload: {
text: string;
meta: Record<string, unknown>;
};
}ChannelType
type ChannelType = "DM" | "GROUP" | "COMMUNITY";ConnectionState
type ConnectionState =
| "disconnected"
| "connecting"
| "connected"
| "reconnecting";LogLevel
type LogLevel = "debug" | "info" | "warn" | "error" | "silent";Error Handling
Error Classes
class RealtimeError extends Error {
name = "RealtimeError";
}
class ConnectionError extends RealtimeError {
name = "ConnectionError";
cause: unknown;
}Common Errors
| Scenario | Error | Handling |
|---|---|---|
| Invalid JWT | HTTP 401 | Get a new token from the auth service |
| Tenant at capacity | HTTP 429 | Wait for other users to disconnect |
| Message too large | "Payload too large" | Split the message or reduce its size |
| Not in group | "You must join the group..." | Call joinGroup() first |
| Token refresh failed | RealtimeError | Re-authenticate with yappa-auth |
WebSocket Protocol
Client → Server Messages
Direct Message
{
"channel_type": "DM",
"user_id": "recipient_user_id",
"content": "Hello!"
}Group Message
{
"channel_type": "GROUP",
"user_id": "group_id",
"content": "Hello team!"
}Group Operations
// Join
{"msg_type":"JOIN","tenant_id":"t1","group_id":"g1","user_id":"alice"}
// Leave
{"msg_type":"LEAVE","tenant_id":"t1","group_id":"g1","user_id":"alice"}
// Create
{"msg_type":"CREATE","tenant_id":"t1","group_id":"new_group","user_id":"alice"}
// Delete
{"msg_type":"DELETE","tenant_id":"t1","group_id":"g1","user_id":"alice"}Server Heartbeat
The server sends Ping frames every 15 seconds. The client should respond with Pong. If there’s no activity for 30 seconds, the server closes the connection.
Tenant Limits
The server enforces MAX_USERS_PER_TENANT (default: 10) using atomic Redis Lua scripts:
tenant_id and user_idSCARD online:{tenant_id} against the limitSADD online:{tenant_id} user_id and INCR conncount:{tenant_id}:{user_id}Redis Keys
| Key | Type | Purpose |
|---|---|---|
online:{tenant_id} | SET | Distinct online user_ids |
conncount:{tenant_id}:{user_id} | STRING (counter) | Connection reference count per user |