KalamDB WebSocket Protocol
May 13, 2026 · View on GitHub
Endpoint: ws://<host>:2900/v1/ws
Transport: RFC 6455 WebSocket
Message encoding: JSON text frames, plus optional gzip-compressed binary frames for large server messages
This document is aligned with the current server implementation in kalamdb-api + kalamdb-commons.
1) Connection and Authentication
1.1 HTTP Upgrade
- Client opens
GET /v1/ws - Connection can be rejected before upgrade when:
- Server is shutting down (
503 Service Unavailable) - Origin is not allowed (
403 Forbidden) - Origin header is required (strict mode) but missing (
403 Forbidden)
- Server is shutting down (
1.2 Origin validation
Origin allow-list is read from security.cors.allowed_origins.
Behavior:
- Empty allow-list = no origin restriction
- Allow-list containing
"*"= any origin - Otherwise origin must exactly match an allowed value
- If
security.strict_ws_origin_check = true, missingOriginheader is rejected
1.3 Post-connect authentication (required)
A connection is unauthenticated after upgrade. Client must send:
{
"type": "authenticate",
"method": "jwt",
"token": "<JWT_TOKEN>"
}
Important:
- Only JWT auth is supported for WebSocket auth messages
- If authentication fails, server sends
auth_errorand closes the connection - Auth timeout is controlled by
websocket.auth_timeout_secs(default3seconds)
Success message:
{
"type": "auth_success",
"user_id": "u_...",
"role": "Dba"
}
Failure message:
{
"type": "auth_error",
"message": "Invalid username or password"
}
2) Frame Behavior and Compression
Server sends JSON in two frame styles:
- Text frames: normal JSON
- Binary frames: gzip-compressed JSON when payload is large enough and compression is beneficial
Current compression behavior:
- Compression threshold:
512bytes - Compression format: gzip
- If compressed payload is not smaller, server keeps plain text frame
Client requirements:
- Must handle both text JSON and binary gzip JSON from server
- Must not send binary frames to server (binary client frames are rejected)
3) Client → Server Messages
All client messages use {"type": ...} tagged JSON.
3.1 Authenticate
{
"type": "authenticate",
"method": "jwt",
"token": "<JWT_TOKEN>"
}
3.2 Subscribe
{
"type": "subscribe",
"subscription": {
"id": "orders_live",
"sql": "SELECT id, status, _seq FROM app.orders WHERE status = 'open'",
"options": {
"batch_size": 500,
"last_rows": 100,
"from_seq_id": 12345
}
}
}
subscription fields:
id(string, required, must not be empty/blank)sql(string, required)options(optional object)batch_size?: numberlast_rows?: numberfrom_seq_id?: number
Option precedence used by server:
- If
from_seq_idexists: resume from that sequence - Else if
last_rowsexists: fetch last N rows - Else: default batch mode
3.3 Next batch
{
"type": "next_batch",
"subscription_id": "orders_live",
"last_seq_id": 12345
}
last_seq_id is optional.
3.4 Unsubscribe
{
"type": "unsubscribe",
"subscription_id": "orders_live"
}
No unsubscribe acknowledgement message is currently emitted.
4) Server → Client Messages
4.1 Authentication result
auth_successauth_error
(see examples above)
4.2 Subscription acknowledgment
{
"type": "subscription_ack",
"subscription_id": "orders_live",
"total_rows": 0,
"batch_control": {
"batch_num": 0,
"has_more": true,
"status": "loading",
"last_seq_id": 12345
},
"schema": [
{"name": "id", "data_type": "BigInt", "index": 0},
{"name": "status", "data_type": "Text", "index": 1},
{"name": "_seq", "data_type": "BigInt", "index": 2}
]
}
Notes:
schemais included insubscription_acktotal_rowsis currently set to0by handler path
4.3 Initial data batch
{
"type": "initial_data_batch",
"subscription_id": "orders_live",
"rows": [
{"id": 1, "status": "open", "_seq": 12346}
],
"batch_control": {
"batch_num": 0,
"has_more": true,
"status": "loading",
"last_seq_id": 12346
}
}
The client resume cursor is always last_seq_id; snapshot and commit
boundaries are backend-owned and are not part of the WebSocket contract.
batch_control.status values:
loading(first batch, more pending)loading_batch(subsequent batch, more pending)ready(initial load complete, live streaming active)
4.4 Change notification
{
"type": "change",
"subscription_id": "orders_live",
"change_type": "insert",
"rows": [
{"id": 2, "status": "open", "_seq": 13001}
]
}
change_type is one of:
insertupdatedelete
Shape rules:
insert:rowspresent,old_valuesabsentupdate: bothrowsandold_valuespresentdelete:old_valuespresent,rowsabsent
4.5 WebSocket error notification
{
"type": "error",
"subscription_id": "orders_live",
"code": "INVALID_SQL",
"message": "..."
}
For protocol/rate-limit level errors, subscription_id may be synthetic values like "protocol", "rate_limit", or "subscribe".
5) Validation and Authorization Rules
5.1 Authentication gating
subscribebefore auth: returns errorAUTH_REQUIREDnext_batchorunsubscribebefore auth: currently ignored (no response)
5.2 Subscription SQL constraints
Current server-side constraints include:
- SQL must resolve to
namespace.tableformat - System namespace subscriptions require
dbaorsystemrole - Shared tables are rejected for live subscriptions
- User and stream tables are allowed (subject to role/permissions)
5.3 Subscription limits
Two limits can apply:
- Per-user configured limit:
rate_limit.max_subscriptions_per_user(default10) - Hard per-connection cap:
100subscriptions
5.4 Message limits and rate limiting
- Max incoming message size:
security.max_ws_message_size(default1MB) - Max message rate per connection:
rate_limit.max_messages_per_sec(default50) - Client binary message frames are rejected (
UNSUPPORTED_DATA)
6) Error Codes
WebSocket error codes currently used:
AUTH_REQUIREDINVALID_SUBSCRIPTION_IDSUBSCRIPTION_LIMIT_EXCEEDEDUNAUTHORIZEDNOT_FOUNDINVALID_SQLUNSUPPORTEDSUBSCRIPTION_FAILEDCONVERSION_ERRORBATCH_FETCH_FAILEDMESSAGE_TOO_LARGERATE_LIMIT_EXCEEDEDUNSUPPORTED_DATAPROTOCOL
7) Timeouts and Keepalive
Connection manager settings:
websocket.auth_timeout_secs(default3)websocket.client_timeout_secs(default10)websocket.heartbeat_interval_secs(default5)
Behavior:
- Server sends ping control frames
- Client should respond with pong (standard WebSocket behavior)
- Heartbeat timeout leads to connection close
8) Failure Behavior Summary
- Invalid JSON message payload: connection is closed with error close code
- Auth timeout:
auth_errorthen close - Auth failure:
auth_errorthen close - Server shutdown: connection closed with shutdown reason
- Oversized/rate-limited/binary client message: server sends
type=errornotification