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.

What a Notification Contains​

Every entity notification is the same small envelope: the method that caused the change, the collection it happened in, and the ids of the affected rows.

{
"action": "PATCH",
"path": "my-api/products",
"ids": ["3f2b8c14-...", "9c1470ab-..."]
}

The envelope never carries field values. path is always the base collection, even when the write came in through a view.

Notifications exist per collection, so _listen on a view or a query is rejected — subscribe on the collection and put the LISTEN rule there.

Once you know something changed, fetch what you need with an ordinary GET.

Access and Scope​

LISTEN is not row-filtered

LISTEN is a collection-level gate. Once a role can subscribe it receives notifications for every row in that collection: security policies, view filters and _CREATOR do not narrow what is delivered. A caller who cannot GET a row can still learn that the row's id changed.

Never grant LISTEN to _CREATOR. It does not mean "notify each user about their own rows" — it hands the whole collection's stream to any authenticated caller.

This is deliberate. Subscriptions live in the messaging service rather than in the API, so when a change is published there is no per-subscriber user context to test a row against — and row visibility is a property of the pair (row, user), never of the row alone.

Notifications therefore carry ids and nothing else: the client is expected to follow up with a read, and a read of a collection or a view applies the security policy and _CREATOR as normal. Field values only ever travel over that filtered read.

Narrowing a Subscription​

note

id and lookupIds let a client ignore traffic it doesn't care about. They are not access control — nothing checks that you may see the ids you subscribe to, and any subscriber can leave both out and receive the whole collection.

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 HTTP Functions​

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

// Function: join-room (HTTP)
// Client calls: POST /api/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 HTTP function
await fetch('/api/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:)

Invoking a Client​

A function can call a method on a connected SignalR client and get the client's return value back, as a plain request and response. The typical use is a small program on a customer's own network, an agent, that holds a SignalR connection to your API and answers questions about data only it can reach, such as an on-premise database. The browser never touches real time: it calls an HTTP function, and the function's HTTP response carries the agent's answer.

browser --HTTP--> your function --/_rt/invoke--> agent (SignalR) --return value--> your function --HTTP--> browser

It takes three pieces.

1. The agent connects and handles a method. Whatever the handler returns is the reply.

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

const connection = new signalR.HubConnectionBuilder()
.withUrl('https://my-api.restapi.cloud/api/_rt/signalr', {
accessTokenFactory: () => agentToken // a token for the agent's own service account
})
.withAutomaticReconnect()
.build();

connection.on('query', async (body) => {
return await runQuery(body.sql, body.params); // the reply
});

await connection.start();
await register(connection.connectionId);
// A reconnect gets a new connection id, and the old one left the channel with the old connection
connection.onreconnected((connectionId) => register(connectionId));

async function register(connectionId) {
await fetch('https://my-api.restapi.cloud/api/agent-join', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${agentToken}` },
body: JSON.stringify({ connectionId })
});
}

2. A function puts the agent in a private channel. Only let the agent's own service account in: whichever client is in the channel receives every call and decides what comes back.

// Function: agent-join (HTTP)
if (me?.id !== secrets.ERP_AGENT_USER_ID) {
res = { status: 403 };
return;
}

await post('/_rt/join', {
connectionId: req.bodyJson.connectionId,
channel: 'agent:erp'
});

res = { status: 200 };

3. A function invokes the channel. It decides who may ask for what and builds the request, so the agent can stay a plain pipe. userId makes sure the call only ever reaches the agent's own connection, whatever else ends up in the channel.

// Function: erp-customers (HTTP)
// Browser calls: POST /api/erp-customers
const rows = await post('/_rt/invoke', {
channel: 'agent:erp',
userId: secrets.ERP_AGENT_USER_ID,
method: 'query',
body: {
sql: 'SELECT id, name FROM customers WHERE region = @region',
params: { region: req.bodyJson.region }
}
});

res = { status: 200, bodyJson: rows };

Request​

FieldRequiredDescription
channelYesA private channel (name containing :)
methodYesThe method name the client registered with connection.on
bodyNoAny JSON. The handler always receives exactly one argument: this, or null
userIdNoOnly invoke a connection this user opened. Recommended whenever you know who the client is
timeoutSecondsNoHow long to wait for the reply, 1 to 90. Default 30

Response​

StatusMeaning
200The client's return value, exactly as the client sent it
400Invalid request, or the token has no right to invoke (see below)
401The function sent no token (see below)
404No client that may be invoked is in the channel, or it disconnected before answering
502The client's handler threw (the message is the client's error), or the call could not be delivered
504No answer within timeoutSeconds

Inside a function, post throws on an error status, and the error message starts with it, for example HTTP 504 Gateway Timeout.

Rules and Limits​

  • Only functions invoke. It takes the realtime rights that the tokens made for functions carry, and so does the token an external function's endpoint receives. A user's own token never has them, so a browser can't invoke a client itself.
  • The function needs a token. post sends the identity from the function's Token Options. With None, or Current User and no signed-in caller, it sends no token and gets 401. Use Service Account if the function can run without a signed-in user.
  • Only private channels. Anyone can join a regular channel, and whoever is in the channel gets the call. The part before the first : can't be the name of one of your collections either: those are the channels entity subscriptions use.
  • Only signed-in clients. A connection opened without a token is never invoked, since anyone can open one.
  • Only your own API's connections. A connection opened through another API is never invoked, even if its connection id ends up in your channel.
  • SignalR only. Web PubSub has no way to return a value to the caller, so its connections can't be invoked.
  • One client answers. If several clients in the channel qualify, one of them gets the call.
  • Requests and replies must each stay under 1 MB. The service drops a client that sends a larger reply, and the call fails with 404. Page large results.
  • At most once. A call is never retried, so a timeout doesn't tell you whether the client acted on it.
tip

Check what the agent may do on its own side too. For a database agent, connect it with a login that can only read the views you mean to expose. Then that login, not your function, is the hard limit on what can be reached.

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/_rt/invokePOSTCall a method on a client in a private channel and return its reply (functions only)
/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
  • On-premise data — An agent on the customer's network answers queries that functions invoke