Skip to Content
API ReferenceReal-time & Subscriptions

Real-time and Subscriptions

Foir provides real-time communication through three mechanisms:

  • GraphQL WebSocket subscriptions on api.foir.io for model change events
  • Server-Sent Events (SSE) on realtime.foir.io for streaming channel events
  • WebSocket event channels on realtime.foir.io for 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/ws

Foir 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

TypeDescription
CREATEDA new record was created
UPDATEDAn existing record was modified
DELETEDA record was deleted
PUBLISHEDA record version was published
UNPUBLISHEDA 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):

ParameterValue
tokenA collab token (admin) or customer JWT
apiKeyA project API key

Available Channels

ChannelDescription
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

SSEWebSocket
DirectionServer to clientBidirectional
ChannelsOne per connectionDynamic subscribe/unsubscribe
ReconnectionBuilt into EventSourceManual or library-managed
AuthQuery parameter (?token=)Query parameter (?token=)
Best forNotification feeds, status streamsCollab, 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/info

Request

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.

Last updated on