Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Real-time WebSocket

Two WebSocket endpoints stream real-time updates: one scoped to a sync device, one general-purpose for UIs. This page covers both in detail.

Endpoints#

Endpoint Purpose
WS /api/v1/sync/ws/{device_id} Sync nudges for a registered device
WS /api/v1/realtime/ws General updates for the authenticated user

Both use WSS (TLS) in production.

Authentication#

WebSocket handshakes can't carry arbitrary headers from browsers, so ScaiDrive accepts the JWT as a query parameter:

text
1
wss://scaidrive.scailabs.ai/api/v1/realtime/ws?token=eyJhbGc...

Non-browser clients can also pass Authorization: Bearer <token> as a header during the upgrade; both methods are honored.

If the token is missing or invalid, the server closes the connection with code 4401.

Sync WebSocket#

Used by clients that replicate content locally. Connects per-device.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import asyncio, json
import websockets

async def run(url_ws, token, device_id):
    url = f"{url_ws}/api/v1/sync/ws/{device_id}?token={token}"
    async with websockets.connect(url) as ws:
        await ws.send(json.dumps({"type": "subscribe"}))
        async for msg in ws:
            frame = json.loads(msg)
            handle(frame)

def handle(frame):
    t = frame.get("event_type") or frame.get("type")
    if t == "change":
        trigger_pull(frame["share_id"])
    elif t == "force_sync":
        trigger_pull(frame["share_id"], full=True)
    elif t == "sync_preference_updated":
        reload_preferences(frame["share_id"])
    elif t == "pong":
        pass
typescript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const ws = new WebSocket(
  `${urlWs}/api/v1/sync/ws/${deviceId}?token=${token}`,
);

ws.addEventListener("open", () => {
  ws.send(JSON.stringify({ type: "subscribe" }));
});

ws.addEventListener("message", (e) => {
  const frame = JSON.parse(e.data);
  switch (frame.event_type ?? frame.type) {
    case "change": triggerPull(frame.share_id); break;
    case "force_sync": triggerPull(frame.share_id, true); break;
    case "sync_preference_updated": reloadPrefs(frame.share_id); break;
  }
});

Server → client frames#

Frame Payload
{"event_type": "change", "share_id": "...", "data": {...}} A change occurred on a share the device is subscribed to
{"event_type": "sync_preference_updated", "share_id": "...", "device_id": "..."} Preferences changed (possibly from another device or admin)
{"event_type": "force_sync", "share_id": "..."} Server requests a full resync
{"type": "pong"} Response to ping
{"type": "subscribed"} Subscription confirmed

The change frame's data is the full change entry (see Sync Model) so clients can often act without a pull. For reliability, clients should still pull the change stream to close any gaps.

Client → server frames#

Frame Effect
{"type": "ping"} Heartbeat; expect pong
{"type": "subscribe"} Subscribe to all shares the authenticated user has access to

The sync WebSocket does not require explicit per-share subscription — subscribing once covers every share the device syncs.

Realtime WebSocket (UIs)#

Used by the web and mobile clients for live UI updates. Requires explicit per-share subscription.

javascript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const ws = new WebSocket(
  `wss://scaidrive.scailabs.ai/api/v1/realtime/ws?token=${token}`,
);

ws.addEventListener("open", () => {
  ws.send(JSON.stringify({ action: "subscribe", share_id: "shr_01J3K" }));
});

ws.addEventListener("message", (e) => {
  const f = JSON.parse(e.data);
  switch (f.type) {
    case "file_created": onCreated(f.share_id, f.data); break;
    case "file_updated": onUpdated(f.share_id, f.data); break;
    case "file_deleted": onDeleted(f.share_id, f.data.file_id); break;
    case "folder_created": onFolder(f.share_id, f.data); break;
    case "folder_deleted": onFolderDeleted(f.share_id, f.data.folder_id); break;
  }
});

Server → client frames#

Frame Payload
connected {user_id, timestamp} — sent once on handshake
subscribed {share_id} — after subscribe
unsubscribed {share_id} — after unsubscribe
file_created {share_id, data: file metadata}
file_updated {share_id, data: file metadata}
file_deleted {share_id, data: {file_id}}
folder_created {share_id, data: folder metadata}
folder_deleted {share_id, data: {folder_id}}
pong Response to ping

Client → server actions#

Action Effect
{"action": "subscribe", "share_id": "..."} Subscribe to a share
{"action": "unsubscribe", "share_id": "..."} Unsubscribe
{"action": "ping"} Heartbeat

Heartbeats#

Send a ping every 30 seconds. If you don't receive a pong within 30 seconds, assume the connection is dead and reconnect.

Most proxy layers (nginx, cloud load balancers) idle out WebSockets after 60 seconds of silence. The heartbeat keeps the connection open.

Reconnection#

WebSockets drop. Plan for it:

  1. On close event, wait with exponential backoff: 1s, 2s, 4s, 8s, max 30s.
  2. Reconnect. The server doesn't persist per-connection state; you re-subscribe and the server re-delivers a subscription confirmation.
  3. After reconnecting, pull the change stream with your current cursor — don't assume the WebSocket delivered every frame while you were disconnected.

The change log is the source of truth. The WebSocket is a low-latency hint.

Back-pressure#

For slow clients, the server buffers a limited number of frames (default 1000 per connection). If the buffer fills, the server sends a force_sync frame and may close the connection. Clients should drain messages promptly, or batch UI updates.

Scaling#

Real-time WebSocket subscriptions go through Redis pub/sub — one ScaiDrive instance can serve many connections, and horizontal scaling is fan-out through Redis. No sticky sessions are required; any instance can handle any WebSocket.

Stats#

For admin / observability:

bash
1
2
curl -H "Authorization: Bearer $SCAIDRIVE_TOKEN" \
     $SCAIDRIVE_URL/api/v1/realtime/stats
json
1
2
3
4
5
6
7
8
{
  "total_connections": 423,
  "subscribed_shares": 87,
  "subscriptions_by_share": {
    "shr_01J3K": 42,
    "shr_01J4Z": 18
  }
}

What's next#

Updated 2026-05-18 15:04:18 View source (.md) rev 2