WebSocket API
Cloudillo provides three WebSocket endpoints for real-time features.
| Endpoint | Purpose | Protocol |
|---|---|---|
/ws/bus |
Notifications and direct messaging | JSON ({ id, cmd, data }) |
/ws/rtdb/{file_id} |
Real-time database sync | JSON ({ id, type, ... }) |
/ws/crdt/{doc_id} |
Collaborative document editing | Binary (Yjs sync protocol) |
Authentication
Tokens are passed as a query parameter:
wss://server.com/ws/bus?token=eyJhbGc...
wss://server.com/ws/rtdb/file_123?token=eyJhbGc...
wss://server.com/ws/crdt/doc_123?token=eyJhbGc...&access=writeAdditional query parameters for RTDB and CRDT:
| Parameter | Values | Description |
|---|---|---|
access |
read, write |
Force access level (default: determined by permissions) |
via |
file ID | Container file ID for embedded access (caps access by share entry) |
Info
The /ws/bus endpoint requires authentication – unauthenticated connections are rejected with close code 4401. The RTDB and CRDT endpoints support guest (unauthenticated) access with read-only permissions for public files.
WebSocket close codes
| Code | Meaning |
|---|---|
4400 |
Invalid store ID format |
4401 |
Unauthorized (authentication required) |
4403 |
Access denied or write access denied |
4404 |
File/document not found |
4409 |
Store type mismatch (e.g. RTDB endpoint for a CRDT file) |
4500 |
Internal server error |
Message bus (/ws/bus)
The bus provides direct user-to-user messaging and notifications. All messages use the format { id, cmd, data }.
Client → Server:
cmd |
Description |
|---|---|
ping |
Keepalive; server responds with ack "pong" |
ACTION |
Send an action; server responds with ack "ok" |
| Any other | Custom command (presence, typing, etc.); server acks with "ok" |
Server → Client:
Messages from other users are forwarded with the same { id, cmd, data } format. The bus does not use channels or subscriptions – it’s a direct messaging system where the server forwards relevant messages to registered users.
Client-side connection
The openMessageBus() function from @cloudillo/core returns a raw WebSocket:
import { openMessageBus } from '@cloudillo/core'
const ws = openMessageBus({ idTag: 'alice', authToken: token })
ws.onmessage = (event) => {
const msg = JSON.parse(event.data)
console.log('Command:', msg.cmd, 'Data:', msg.data)
}
// Send a command
ws.send(JSON.stringify({ id: '1', cmd: 'ping', data: {} }))Real-time database (/ws/rtdb/{file_id})
Real-time synchronization of structured data. All messages use the format { id, type, ...payload }. See RTDB for the client library documentation.
Client → server messages
| Type | Key fields | Description |
|---|---|---|
transaction |
operations: [{type, path, data}] |
Atomic batch of create/update/replace/delete operations |
query |
path, filter?, sort?, limit?, offset?, aggregate? |
Query documents with optional filtering and aggregation |
get |
path |
Get a single document |
subscribe |
path, filter?, aggregate? |
Start real-time change notifications for a path |
unsubscribe |
subscriptionId |
Stop receiving change notifications |
lock |
path, mode |
Lock a document ("soft" or "hard") |
unlock |
path |
Release a document lock |
ping |
– | Keepalive |
Server → client messages
| Type | Key fields | Description |
|---|---|---|
ack |
status, timestamp |
Acknowledges a transaction/command |
transactionResult |
results: [{ref?, id}] |
Per-operation results from a transaction |
queryResult |
data: [...] |
Query results |
getResult |
data |
Single document result |
subscribeResult |
subscriptionId, data |
Initial subscription data |
change |
subscriptionId, event: {action, path, data?} |
Real-time change notification |
lockResult |
locked, holder?, mode? |
Lock operation result |
pong |
– | Keepalive response |
error |
code, message |
Error response |
Transaction operations
Each operation in a transaction has a type and a path:
| Operation | Description |
|---|---|
create |
Create a document; returns generated ID. Supports ref for cross-referencing within the transaction |
update |
Shallow merge (Firebase-style) with existing document |
replace |
Full document replacement (no merge) |
delete |
Delete a document |
All operations in a transaction are atomic – if any fails, the entire transaction rolls back. Operations support computed values ($op, $fn, $query) and reference substitution (${$ref} patterns) for creating related documents in a single transaction.
Change event actions
The event.action field in change notifications can be: create, update, delete, lock, unlock, or ready.
Client-side connection
The openRTDB() function from @cloudillo/core returns a raw WebSocket. For higher-level usage, use the @cloudillo/rtdb client library:
import { createRtdbClient } from '@cloudillo/rtdb'
import { getRtdbUrl } from '@cloudillo/core'
const db = createRtdbClient({
dbId: fileId,
auth: { getToken: () => bus.accessToken },
serverUrl: getRtdbUrl(bus.idTag!, fileId, bus.accessToken!)
})Store files
RTDB supports auto-created store files using the pattern s~{app_id} (e.g. s~taskillo). These are created automatically on first WebSocket connection, providing persistent app-specific data storage without manual file creation.
Collaborative documents (/ws/crdt/{doc_id})
CRDT synchronization using the Yjs binary protocol. See CRDT for full documentation.
Protocol
The endpoint uses the y-websocket binary protocol:
| Message type | Code | Description |
|---|---|---|
MSG_SYNC |
0 |
Sync protocol (SyncStep1, SyncStep2, Update) |
MSG_AWARENESS |
1 |
User presence and cursor updates |
The sync flow:
- Client sends SyncStep1 (state vector)
- Server responds with SyncStep2 (missing updates)
- Both sides exchange Updates incrementally as edits happen
- Awareness messages broadcast cursor positions and user presence
Read-only connections can receive sync and awareness data but cannot send Update messages.
Client-side connection
Use openYDoc() from @cloudillo/crdt, which handles authentication, client ID reuse, offline caching, and token refresh automatically:
import { openYDoc } from '@cloudillo/crdt'
import * as Y from 'yjs'
const yDoc = new Y.Doc()
const { provider, persistence, offlineCached } = await openYDoc(yDoc, 'ownerTag:docId')
// Access shared types
const yText = yDoc.getText('content')
// Awareness is available via provider.awareness
Note
openYDoc() automatically handles WebSocket close codes: on 4401 (unauthorized) it requests a fresh token from the shell and reconnects. On other 44xx errors it stops reconnection and notifies the shell via bus.notifyError().
Connection lifecycle
All three endpoints share common behaviors:
- Heartbeat: Server sends WebSocket ping frames every 30 seconds
- Multi-tab support: Each connection gets a unique
conn_id; multiple connections per user are supported - Cleanup: On disconnect, locks are released (RTDB), subscriptions cancelled, and user unregistered (bus)
- Activity tracking: File access and modification times are recorded (throttled to 60-second intervals)
See also
- RTDB – real-time database client library
- CRDT – collaborative editing with Yjs
- Authentication – token management