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=write

Additional 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:

  1. Client sends SyncStep1 (state vector)
  2. Server responds with SyncStep2 (missing updates)
  3. Both sides exchange Updates incrementally as edits happen
  4. 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