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:
1 | |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
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:
- On
closeevent, wait with exponential backoff: 1s, 2s, 4s, 8s, max 30s. - Reconnect. The server doesn't persist per-connection state; you re-subscribe and the server re-delivers a subscription confirmation.
- 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:
1 2 | |
1 2 3 4 5 6 7 8 | |