Real-time and Subscriptions
Foir provides real-time communication through three mechanisms:
- GraphQL WebSocket subscriptions on
api.foir.iofor model change events - Server-Sent Events (SSE) on
realtime.foir.iofor streaming channel events - WebSocket event channels on
realtime.foir.iofor bidirectional real-time (collab, presence, dynamic subscriptions)
GraphQL WebSocket Subscriptions
GraphQL subscriptions use the same origin as queries and mutations, so your client setup is straightforward.
Endpoint
wss://api.foir.io/graphql/wsFoir uses the graphql-ws protocol for WebSocket subscriptions.
Connection Setup
Initialize the connection with your API key in the connection parameters:
import { createClient } from "graphql-ws";
const client = createClient({
url: "wss://api.foir.io/graphql/ws",
connectionParams: {
apiKey: "pk_live_your_api_key",
},
});If you are using customer authentication, include the customer JWT as well:
const client = createClient({
url: "wss://api.foir.io/graphql/ws",
connectionParams: {
apiKey: "pk_live_your_api_key",
authorization: `Bearer ${customerAccessToken}`,
},
});Subscribing to Model Changes
Subscribe to real-time change events for specific models:
const unsubscribe = client.subscribe(
{
query: `
subscription OnRecordChange($modelKey: String!) {
recordChanged(modelKey: $modelKey) {
type
record {
id
modelKey
naturalKey
data
updatedAt
}
}
}
`,
variables: { modelKey: "product" },
},
{
next(data) {
console.log("Record changed:", data);
},
error(err) {
console.error("Subscription error:", err);
},
complete() {
console.log("Subscription complete");
},
}
);
// Later, to unsubscribe:
unsubscribe();Change Event Types
| Type | Description |
|---|---|
CREATED | A new record was created |
UPDATED | An existing record was modified |
DELETED | A record was deleted |
PUBLISHED | A record version was published |
UNPUBLISHED | A record was unpublished |
Using with Apollo Client
import { ApolloClient, InMemoryCache, split, HttpLink } from "@apollo/client";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
import { getMainDefinition } from "@apollo/client/utilities";
const httpLink = new HttpLink({
uri: "https://api.foir.io/graphql",
headers: {
"x-api-key": "pk_live_your_api_key",
},
});
const wsLink = new GraphQLWsLink(
createClient({
url: "wss://api.foir.io/graphql/ws",
connectionParams: {
apiKey: "pk_live_your_api_key",
},
})
);
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
httpLink
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});Connection Lifecycle
The graphql-ws client handles reconnection automatically. You can configure retry behavior:
const client = createClient({
url: "wss://api.foir.io/graphql/ws",
connectionParams: {
apiKey: "pk_live_your_api_key",
},
retryAttempts: 5,
retryWait: async (retryCount) => {
await new Promise((resolve) =>
setTimeout(resolve, Math.min(1000 * 2 ** retryCount, 16000))
);
},
on: {
connected: () => console.log("WebSocket connected"),
closed: () => console.log("WebSocket closed"),
error: (err) => console.error("WebSocket error:", err),
},
});Server-Sent Events (SSE)
The SSE endpoint on the realtime service provides a lightweight, read-only stream for any event channel. Each SSE connection subscribes to one channel. Use this for customer-facing features like notification inboxes, order status feeds, or content change streams.
Endpoint
GET https://realtime.foir.io/sse/{channel}Authentication
Pass a token or API key as a query parameter (SSE connections cannot set headers):
| Parameter | Value |
|---|---|
token | A collab token (admin) or customer JWT |
apiKey | A project API key |
Available Channels
| Channel | Description |
|---|---|
notifications:{userId} | Notifications for a user (scoped to own user) |
changes:{modelKey} | Record changes for a model |
changes:* | All record changes |
jobs:{jobId} | Job progress updates |
jobs:* | All job updates |
Connecting with EventSource
// Customer notification inbox
const source = new EventSource(
`https://realtime.foir.io/sse/notifications:${customerId}?token=${customerToken}`
);
source.addEventListener("connected", (event) => {
const { channel, userId } = JSON.parse(event.data);
console.log(`Connected to ${channel}`);
});
source.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
console.log("Event:", data.event, data.data);
});
source.onerror = () => {
// EventSource reconnects automatically
};Connecting with fetch
For environments where you need more control:
async function connectSSE(channel, token) {
const response = await fetch(
`https://realtime.foir.io/sse/${channel}?token=${token}`,
{ headers: { Accept: "text/event-stream" } }
);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n\n");
buffer = lines.pop();
for (const block of lines) {
if (block.startsWith(": heartbeat")) continue;
const dataMatch = block.match(/^data: (.+)$/m);
if (dataMatch) {
const event = JSON.parse(dataMatch[1]);
handleEvent(event);
}
}
}
}Multiple Channels
If you need events from multiple channels, open multiple connections:
// Listen to both notifications and product changes
const notifications = new EventSource(
`https://realtime.foir.io/sse/notifications:${userId}?token=${token}`
);
const productChanges = new EventSource(
`https://realtime.foir.io/sse/changes:product?apiKey=${apiKey}`
);For dynamic multi-channel subscriptions, use the WebSocket event channel instead.
Event Format
All events are JSON objects with channel, event, and data fields:
{
"channel": "notifications:usr_abc123",
"event": "notification.new",
"data": {
"id": "notif_abc123",
"type": "order_update",
"title": "Order Shipped",
"message": "Your order #1234 has been shipped.",
"userId": "usr_abc123"
}
}Heartbeats are sent every 30 seconds as SSE comments (: heartbeat) to keep the connection alive.
WebSocket Event Channels
For bidirectional real-time with dynamic subscribe/unsubscribe, use the WebSocket event channel on the realtime service.
Endpoint
wss://realtime.foir.io/_events?token=...Connection
const token = await getCollabToken(); // or customer JWT, or API key via ?apiKey=
const ws = new WebSocket(`wss://realtime.foir.io/_events?token=${token}`);
ws.onopen = () => {
// Subscribe to channels
ws.send(JSON.stringify({ type: "subscribe", channel: "notifications:usr_abc123" }));
ws.send(JSON.stringify({ type: "subscribe", channel: "changes:product" }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(`[${data.channel}] ${data.event}:`, data.data);
};
// Unsubscribe from a channel
ws.send(JSON.stringify({ type: "unsubscribe", channel: "changes:product" }));SSE vs WebSocket
| SSE | WebSocket | |
|---|---|---|
| Direction | Server to client | Bidirectional |
| Channels | One per connection | Dynamic subscribe/unsubscribe |
| Reconnection | Built into EventSource | Manual or library-managed |
| Auth | Query parameter (?token=) | Query parameter (?token=) |
| Best for | Notification feeds, status streams | Collab, presence, multi-channel apps |
Collaboration Info
The collaboration info endpoint provides connection details for real-time collaborative editing.
Endpoint
GET https://api.foir.io/collab/infoRequest
curl https://api.foir.io/collab/info \
-H "x-api-key: pk_live_your_api_key"Response
{
"enabled": true,
"provider": "yjs",
"websocketUrl": "wss://realtime.foir.io"
}This endpoint returns the WebSocket URL for the Yjs collaboration protocol used by the content editor.