Skip to content

WebSocket Real-Time API

Overview

ODP provides a WebSocket endpoint for real-time data subscriptions. Clients can subscribe to changes on specific collections and receive push notifications when items are created, updated, or deleted.

The WebSocket endpoint is implemented using @fastify/websocket and a custom WebSocketService singleton.


Connection

Endpoint

ws://api.example.com/websocket

or with TLS:

wss://api.example.com/websocket

Authentication Modes

Configured via WEBSOCKETS_REST_AUTH:

ModeDescription
strictToken required in query param at connection time. Invalid/missing token → connection refused (close code 4401)
handshakeToken optional at connection time. Client can authenticate after connecting via auth message
publicSame as handshake — unauthenticated clients connect but get limited access

Token via query parameter:

wss://api.example.com/websocket?access_token=<JWT>

Message Protocol

All messages are JSON-encoded objects. Every message has a type field.

Message Types

TypeDirectionDescription
authClient → ServerAuthenticate using access token
subscribeClient → ServerSubscribe to a collection
unsubscribeClient → ServerCancel a subscription
pingClient → ServerKeepalive ping
pongServer → ClientKeepalive pong
itemsServer → ClientReal-time item change notification
errorServer → ClientError response

Authentication Message

When using handshake or public mode, send an auth message after connecting:

Client → Server:

json
{
  "type": "auth",
  "uid": "optional-request-id",
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Server → Client (success):

json
{
  "type": "auth",
  "uid": "optional-request-id",
  "data": { "authenticated": true }
}

Server → Client (failure):

json
{
  "type": "error",
  "uid": "optional-request-id",
  "message": "Invalid access token"
}

Subscribe

Subscribe to real-time changes on a collection.

Client → Server:

json
{
  "type": "subscribe",
  "uid": "my-subscription-1",
  "collection": "articles"
}
FieldTypeRequiredDescription
type"subscribe"YesMessage type
uidstringYesUnique subscription ID (client-defined)
collectionstringYesCollection name to subscribe to
queryobjectNoOptional filter query

Server → Client (confirmation):

json
{
  "type": "subscribe",
  "uid": "my-subscription-1",
  "data": {
    "subscribed": true,
    "collection": "articles"
  }
}

Unsubscribe

Cancel a subscription.

Client → Server:

json
{
  "type": "unsubscribe",
  "uid": "my-subscription-1"
}

Server → Client:

json
{
  "type": "unsubscribe",
  "uid": "my-subscription-1",
  "data": { "unsubscribed": true }
}

If the subscription did not exist, unsubscribed is false.


Item Change Notifications

When an item in a subscribed collection is created, updated, or deleted, all subscribed clients receive a push message:

Server → Client (create):

json
{
  "type": "items",
  "event": "create",
  "collection": "articles",
  "uid": "my-subscription-1",
  "data": {
    "keys": ["new-item-uuid"],
    "payload": { "id": "new-item-uuid", "title": "New Article", "status": "draft" }
  }
}

Server → Client (update):

json
{
  "type": "items",
  "event": "update",
  "collection": "articles",
  "uid": "my-subscription-1",
  "data": {
    "keys": ["item-uuid-1", "item-uuid-2"],
    "payload": { "status": "published" }
  }
}

Server → Client (delete):

json
{
  "type": "items",
  "event": "delete",
  "collection": "articles",
  "uid": "my-subscription-1",
  "data": {
    "keys": ["deleted-item-uuid"]
  }
}

Heartbeat (Ping/Pong)

ODP sends periodic ping messages to all connected clients. Clients should respond with pong to prevent disconnection.

Server → Client (server-initiated ping):

json
{ "type": "ping" }

Client → Server (client-initiated ping):

json
{ "type": "ping" }

Server → Client (response to client ping):

json
{ "type": "pong" }

Stale Connection Detection

The heartbeat timer (period: WEBSOCKETS_HEARTBEAT_PERIOD * 1000 ms) runs periodically and:

  1. Closes clients that have not responded within 2 × heartbeat_period (close code 4408)
  2. Sends ping to all remaining clients

Connection Lifecycle


Error Messages

MessageCause
Invalid JSONMalformed JSON message
Missing message typeMessage has no type field
Missing access_tokenauth message sent without access_token
Invalid access tokenToken validation failed
Missing collectionsubscribe message has no collection
Missing uid for subscriptionsubscribe message has no uid
Missing uid for unsubscribeunsubscribe message has no uid
Unknown message type: <type>Unrecognized message type

Configuration

VariableDefaultDescription
WEBSOCKETS_ENABLEDtrueEnable/disable WebSocket endpoint
WEBSOCKETS_HEARTBEAT_ENABLEDtrueEnable heartbeat
WEBSOCKETS_HEARTBEAT_PERIOD30Heartbeat interval in seconds
WEBSOCKETS_REST_AUTHhandshakeAuth mode: public, handshake, strict
WEBSOCKETS_REST_ENABLEDtrueEnable REST-style WebSocket
WEBSOCKETS_GRAPHQL_ENABLEDtrueEnable GraphQL WebSocket (if implemented)
WEBSOCKETS_GRAPHQL_AUTHhandshakeAuth mode for GraphQL WS

Event Sources

WebSocket notifications are driven by ODP's internal event emitter. The WebSocketService listens to:

Emitter EventWS Event
items.createcreate
items.updateupdate
items.deletedelete

These events are fired by ItemsService after successful mutations, so all writes through the standard REST API, GraphQL, and MCP will trigger WebSocket notifications.


JavaScript Client Example

javascript
const ws = new WebSocket('wss://api.example.com/websocket');

ws.onopen = () => {
  // Authenticate
  ws.send(JSON.stringify({
    type: 'auth',
    access_token: 'your-access-token'
  }));
};

ws.onmessage = ({ data }) => {
  const msg = JSON.parse(data);

  if (msg.type === 'auth' && msg.data?.authenticated) {
    // Subscribe to articles collection
    ws.send(JSON.stringify({
      type: 'subscribe',
      uid: 'articles-sub',
      collection: 'articles'
    }));
  }

  if (msg.type === 'items') {
    console.log(`${msg.event} in ${msg.collection}:`, msg.data);
  }

  if (msg.type === 'ping') {
    ws.send(JSON.stringify({ type: 'ping' }));
  }
};

ODP Internal API Documentation