Skip to main content

Real Time

RestAPI.com provides real-time capabilities for detecting database changes and enabling direct client-to-client communication.

Prerequisites

To use real-time subscriptions on a collection, you must configure LISTEN access:

{ "method": "LISTEN", "roleNames": ["_AUTHENTICATED_USER"] }

Without LISTEN access, clients cannot subscribe to changes on that collection. Configure this in the Developer Portal under your collection's Access settings, or in your schema JSON.

Features

  • Database change detection — Subscribe to collection changes as they happen
  • Client-to-client messaging — Direct peer-to-peer communication via channels
  • Server-to-client messaging — Push updates from functions to connected clients

Connection Options

Choose your preferred protocol:

ProtocolLibrary
Azure Web PubSub@azure/web-pubsub-client
SignalR@microsoft/signalr

Setting Up Connections

Azure Web PubSub

import { WebPubSubClient } from '@azure/web-pubsub-client';

const client = new WebPubSubClient({
getClientAccessUrl: async () => {
const response = await fetch('/api/_rt/ws');
return response.text();
}
});

await client.start();

SignalR

import * as signalR from '@microsoft/signalr';

const connection = new signalR.HubConnectionBuilder()
.withUrl('/api/_rt/signalr')
.withAutomaticReconnect()
.build();

await connection.start();

Listening for Changes

SignalR

connection.on('change', (data) => {
console.log('Data changed:', data);
});

Web PubSub

client.on('group-message', (event) => {
console.log('Message received:', event.message);
});

Entity Subscriptions

Subscribe to database changes on a collection using the /_listen endpoint:

await fetch('/api/products/_listen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
connectionId: myConnectionId,
type: 'ws' // or 'signalr'
})
});

Entity subscriptions are read-only — clients receive notifications but cannot send messages to entity channels. This ensures data integrity since entity notifications are controlled by the server.

Filtering Subscriptions

Subscribe to a specific item:

body: JSON.stringify({
connectionId: myConnectionId,
type: 'ws',
id: 'item-uuid' // Only receive changes for this item
})

Filter by lookup relationships:

body: JSON.stringify({
connectionId: myConnectionId,
type: 'ws',
lookupIds: ['parent-uuid-1', 'parent-uuid-2'] // Filter by parent references
})

Channel-Based Communication

Channels enable direct client-to-client messaging. There are two types:

Regular Channels

Standard channels where clients can join and send messages freely:

// Join a channel
await fetch('/api/_rt/join', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
connectionId: myConnectionId,
channel: 'my-room'
})
});

Sending Messages

Direct Communication is Fastest

For the best performance, send messages directly through Web PubSub or SignalR rather than using the /_rt/send API endpoint. Direct communication bypasses the HTTP layer entirely, resulting in lower latency and better throughput.

Recommended: Direct via Web PubSub or SignalR

// Web PubSub - channel prefixed with API name
client.sendToGroup('my-api:my-room', { type: 'chat', text: 'Hello!' });

// SignalR
connection.invoke('SendToGroup', 'my-api:my-room', { type: 'chat', text: 'Hello!' });

Alternative: Via /_rt/send API

Only use the /_rt/send endpoint when:

  • Sending from server-side functions (which don't have a WebSocket connection)
  • The client is not connected to Web PubSub or SignalR
await fetch('/api/_rt/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
channel: 'my-room',
body: { type: 'chat', text: 'Hello!' }
})
});

Private Channels (Server-Controlled)

Channels with : in the name are private channels — they can only be managed via server-side functions. Use these when you need to control who can join or send messages.

Same Endpoints, Different Access

Functions use the same /_rt/join and /_rt/send endpoints as clients. The difference is that functions can access private channels (names containing :) while clients cannot.

Managing Private Channels with Invocable Functions

The recommended pattern is to create an invocable function that validates access and manages channel membership:

// Function: join-room (invocable)
// Client calls: POST /api/_invoke/join-room

const roomId = req.bodyJson.roomId;
const connectionId = req.bodyJson.connectionId;

// Validate access (e.g., check if user is allowed in this room)
const room = await get(`rooms/${roomId}`);
if (!room.members.includes(me.id)) {
res = { status: 403, bodyJson: { error: 'Not a member of this room' } };
return;
}

// Add client to private channel
await post('/_rt/join', {
connectionId: connectionId,
channel: `room:${roomId}`,
readOnly: false // Client can send messages
});

res = { status: 200, bodyJson: { joined: true } };

Client code:

// Get WebSocket connection first
const wsResponse = await fetch('/api/_rt/ws');
const { url } = await wsResponse.json();
const client = new WebPubSubClient(url);
await client.start();

// Request to join a private channel via invocable function
await fetch('/api/_invoke/join-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
roomId: 'my-room-id',
connectionId: client.connectionId
})
});

// Now client can send/receive on the private channel

The readOnly Parameter

Control whether clients can send messages after joining:

// Client can send and receive
await post('/_rt/join', {
connectionId: connectionId,
channel: 'chat:room-123',
readOnly: false
});

// Client can only receive (broadcast pattern)
await post('/_rt/join', {
connectionId: connectionId,
channel: 'announcements:system',
readOnly: true
});

Sending from Functions

Functions can send to any channel, including private channels:

await post('/_rt/send', {
channel: 'announcements:system',
body: { type: 'alert', text: 'Server maintenance in 10 minutes' }
});

Why Clients Cannot Access Private Channels

Clients are blocked from joining or sending to channels containing ::

// This will fail with 400 Bad Request
await fetch('/api/_rt/join', {
body: JSON.stringify({
connectionId: myConnectionId,
channel: 'room:123' // Rejected - contains ':'
})
});

This ensures your function logic always controls access to private channels.

Channel Type Summary

Channel TypeClient JoinClient SendFunction JoinFunction Send
Regular (my-room)YesYesYesYes
Private (room:123)NoVia functionYesYes
Entity (/_listen)Via endpointNoN/AAutomatic

Channel Naming Rules

  • Channel names with : are private (function-controlled only)
  • Channel names cannot match collection names (use /_listen for entities)
  • Channel names are scoped to your API (internally prefixed with api-name:)

Endpoints

EndpointMethodDescription
/api/_rt/wsGETGet WebSocket URL for Web PubSub
/api/_rt/signalrGETGet SignalR connection info
/api/_rt/joinPOSTJoin a channel
/api/_rt/sendPOSTSend message to a channel
/api/{collection}/_listenPOSTSubscribe to entity changes

Use Cases

  • Live dashboards — Update metrics in real time
  • Chat applications — Direct messaging between users
  • Collaborative editing — Sync changes across clients
  • Notifications — Push updates to connected users
  • Gaming — Real-time game state synchronization