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:
| Protocol | Library |
|---|---|
| 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-filteredLISTEN 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
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
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.
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 Type | Client Join | Client Send | Function Join | Function Send |
|---|---|---|---|---|
Regular (my-room) | Yes | Yes | Yes | Yes |
Private (room:123) | No | Via function | Yes | Yes |
Entity (/_listen) | Via endpoint | No | N/A | Automatic |
Channel Naming Rules
- Channel names with
:are private (function-controlled only) - Channel names cannot match collection names (use
/_listenfor 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
| Field | Required | Description |
|---|---|---|
channel | Yes | A private channel (name containing :) |
method | Yes | The method name the client registered with connection.on |
body | No | Any JSON. The handler always receives exactly one argument: this, or null |
userId | No | Only invoke a connection this user opened. Recommended whenever you know who the client is |
timeoutSeconds | No | How long to wait for the reply, 1 to 90. Default 30 |
Response
| Status | Meaning |
|---|---|
200 | The client's return value, exactly as the client sent it |
400 | Invalid request, or the token has no right to invoke (see below) |
401 | The function sent no token (see below) |
404 | No client that may be invoked is in the channel, or it disconnected before answering |
502 | The client's handler threw (the message is the client's error), or the call could not be delivered |
504 | No 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.
postsends the identity from the function's Token Options. With None, or Current User and no signed-in caller, it sends no token and gets401. 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.
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
| Endpoint | Method | Description |
|---|---|---|
/api/_rt/ws | GET | Get WebSocket URL for Web PubSub |
/api/_rt/signalr | GET | Get SignalR connection info |
/api/_rt/join | POST | Join a channel |
/api/_rt/send | POST | Send message to a channel |
/api/_rt/invoke | POST | Call a method on a client in a private channel and return its reply (functions only) |
/api/{collection}/_listen | POST | Subscribe 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