SDK

Subscribing to Topics

Subscribe to domain topics with optional filters to receive real-time events.

The subscription system is the primary way clients receive real-time events. You subscribe to domain topics and the platform delivers matching events from any source (database CDC, sync, or socket).

subscribe(options)

subscribe(options: SubscribeOptions): string
ParameterTypeRequiredDescription
optionsSubscribeOptionsYesSubscription configuration (see fields below)

SubscribeOptions fields:

FieldTypeRequiredDescription
topicstringYesDomain topic name to subscribe to (e.g. "session.status", "order.updated")
filterSubscriptionFilterNoKey-value filter object — only events whose payload matches every filter key are delivered to this client
returnModeSyncEventReturnModeNoControls what data is included in sync document change events: 'full' (default), 'diff', or 'both'. See Return Mode below.

Returns: string — a unique subscription ID (e.g. "sub_1", "sub_2") that you pass to unsubscribe() later.

const subId = realtime.subscribe({
  topic: 'session.status',
});
console.log(subId); // "sub_1"

Listening for Events

on('event', handler) — Global Listener

Receive all events across all active subscriptions:

realtime.on('event', (event: RealtimeEvent) => {
  console.log('Topic:', event.topic);     // e.g. "session.status"
  console.log('Source:', event.source);    // "database" | "sync" | "socket"
  console.log('Payload:', event.payload); // The actual event data
});

on('event:<topic>', handler) — Topic-Specific Listener

Listen for events on a single topic only:

realtime.on('event:session.status', (event: RealtimeEvent) => {
  console.log('Session update:', event.payload);
});

realtime.on('event:order.updated', (event: RealtimeEvent) => {
  console.log('Order update:', event.payload);
});

Subscription Filters

Filters narrow down which events you receive after topic routing. Only events whose payload matches the filter are delivered to your client.

SubscriptionFilter

type SubscriptionFilter = Record<string, unknown>;

Filter values can be:

  • Scalar — exact equality match against the payload field
  • { in: [...] } — match if the payload field equals any value in the array

All filter keys use AND logic — every key must match for the event to be delivered.

Equality Filter

realtime.subscribe({
  topic: 'session.status',
  filter: { campusId: 12 },
});
// Only receives events where payload.campusId === 12

Multiple Fields (AND)

realtime.subscribe({
  topic: 'session.status',
  filter: {
    campusId: 12,
    status: 'active',
  },
});
// Both conditions must match

in Operator

Match against multiple possible values for a single field:

realtime.subscribe({
  topic: 'session.status',
  filter: {
    status: { in: ['pending', 'active'] },
  },
});
// Receives events where status is 'pending' OR 'active'

Combined Filters

realtime.subscribe({
  topic: 'session.status',
  filter: {
    campusId: 12,
    status: { in: ['pending', 'active'] },
  },
});
// campusId must be 12 AND status must be 'pending' or 'active'

Return Mode

When subscribing to sync document topics (e.g. sync.session.updated), you can control how much data each event carries over the wire using the returnMode option:

Modepayload.datapayload.diffDescription
'full' (default)Full document state — backward-compatible default
'diff'Only what changed — minimal wire traffic
'both'Full state + change context

Full Mode (Default)

Receive the complete document data on every change. This is the default and requires no extra configuration:

realtime.subscribe({ topic: 'sync.session.updated' });

realtime.on('event:sync.session.updated', (event) => {
  console.log(event.payload.data); // Full document: { status: 'active', count: 5 }
});

Diff Mode

Receive only the changes — ideal for large documents where you want to minimize bandwidth:

realtime.subscribe({
  topic: 'sync.session.updated',
  returnMode: 'diff',
});

realtime.on('event:sync.session.updated', (event) => {
  console.log(event.payload.diff);
  // {
  //   added: { newField: 42 },
  //   removed: ['oldField'],
  //   changed: { status: { from: 'active', to: 'ended' } }
  // }
});

Both Mode

Receive both the full document and the diff:

realtime.subscribe({
  topic: 'sync.session.updated',
  returnMode: 'both',
});

realtime.on('event:sync.session.updated', (event) => {
  console.log(event.payload.data); // Full document
  console.log(event.payload.diff); // What changed
});

Diff Shape

The diff object has the following structure:

interface JsonDiff {
  added: Record<string, unknown>;   // Keys present in new but not old
  removed: string[];                // Keys present in old but not new
  changed: Record<string, {         // Keys present in both with different values
    from: unknown;
    to: unknown;
  }>;
}

Note: returnMode only affects sync-sourced events. Non-sync events (database CDC, socket) are delivered unchanged regardless of this setting.

unsubscribe(subscriptionId)

unsubscribe(subscriptionId: string): void
ParameterTypeRequiredDescription
subscriptionIdstringYesThe subscription ID returned by subscribe()

Returns: void — the server stops delivering events for this subscription.

const subId = realtime.subscribe({ topic: 'session.status' });

// Later, when you no longer need these events:
realtime.unsubscribe(subId);

Multiple Subscriptions

You can hold multiple active subscriptions simultaneously. Each returns its own subId:

const sub1 = realtime.subscribe({ topic: 'session.status' });
const sub2 = realtime.subscribe({ topic: 'order.updated' });
const sub3 = realtime.subscribe({ topic: 'chat.room.message', filter: { roomId: 'room1' } });

// All events arrive through the same listener
realtime.on('event', (event) => {
  switch (event.topic) {
    case 'session.status':
      handleSession(event);
      break;
    case 'order.updated':
      handleOrder(event);
      break;
    case 'chat.room.message':
      handleChat(event);
      break;
  }
});

// Unsubscribe individually
realtime.unsubscribe(sub3);

Reconnection Behavior

When the WebSocket connection drops and reconnects, all active subscriptions are automatically re-established. You don't need to re-subscribe manually. The client internally tracks all subscription IDs and options, and re-emits them to the server on reconnect.

realtime.on('connection', ({ state }) => {
  if (state === 'reconnecting') {
    console.log('Reconnecting... subscriptions will be restored');
  }
  if (state === 'connected') {
    console.log('Connected — all subscriptions active');
  }
});

Event Envelope

Every event delivered to subscribers follows the standard RealtimeEvent envelope:

interface RealtimeEvent {
  id: string;                       // Unique event ID (e.g. "evt_a1b2c3d4")
  topic: string;                    // Domain topic (e.g. "session.status")
  source: 'database' | 'sync' | 'socket'; // Which service produced the event
  type: string;                     // Event type (e.g. "database.update")
  payload: Record<string, unknown>; // The actual event data
  metadata: {
    timestamp: number;              // Unix timestamp in milliseconds
    table?: string;                 // Source table name (database events only)
    operation?: 'insert' | 'update' | 'delete'; // DB operation (database events only)
    revision?: number;              // Document revision (sync events only)
  };
}