Skip to main content

WebSocket — Real-Time Records

Fyso exposes a WebSocket endpoint for real-time record change events. When a record is created, updated, or deleted, subscribed clients receive a push notification instantly.

Connection

EnvironmentURL
Productionwss://api.fyso.dev/ws
Developmentws://localhost:3001/ws

The connection is tenant-scoped. The server identifies the tenant from the authenticated token presented during the handshake.

Authentication

The first message sent after connecting must be an auth message. The connection is not usable until authentication succeeds.

Three token types are accepted:

{ "type": "auth", "token": "fyso_pkey_..." }
{ "type": "auth", "token": "550e8400-e29b-41d4-a716-446655440000" }
{ "type": "auth", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }
Token formatDescription
fyso_pkey_...Platform API key (recommended for applications)
UUID (session token)Admin session token from the auth endpoint
eyJ...Bot JWT issued by POST /api/auth/bots/identify

Protocol Messages

All messages are JSON objects with a type field. Text frames only — binary frames are not supported.

auth

Sent by client. Must be the first message.

{ "type": "auth", "token": "<token>" }

Server responses:

Success:

{ "type": "auth_ok" }

Failure:

{ "type": "auth_error", "code": "not_authenticated", "message": "Invalid or expired token" }

subscribe

Subscribe to real-time events for an entity. Must be sent after a successful auth.

{ "type": "subscribe", "entity": "tickets" }

Server responses:

Success:

{ "type": "subscribed", "entity": "tickets" }

Limit exceeded:

{
"type": "error",
"code": "limit_exceeded",
"message": "A single connection may subscribe to at most 10 entities."
}

Entity not found:

{ "type": "error", "code": "entity_not_found", "message": "Entity 'tickets' does not exist" }

unsubscribe

Stop receiving events for an entity.

{ "type": "unsubscribe", "entity": "tickets" }

Server response:

{ "type": "unsubscribed", "entity": "tickets" }

ping / pong

Keep the connection alive. Useful for environments that aggressively close idle WebSockets.

{ "type": "ping" }

Server response:

{ "type": "pong" }

Server push events

The server pushes events whenever a record changes in a subscribed entity.

record.created

{
"type": "record.created",
"entity": "tickets",
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"title": "Fix login bug",
"status": "open",
"created_at": "2026-03-07T12:00:00.000Z"
}
}

record.updated

{
"type": "record.updated",
"entity": "tickets",
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "closed",
"updated_at": "2026-03-07T13:00:00.000Z"
}
}

record.deleted

{
"type": "record.deleted",
"entity": "tickets",
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}

Event Data Shape

{
"type": "record.created" | "record.updated" | "record.deleted",
"entity": "<entity_name>",
"data": {
"id": "<uuid>",
"<field>": "<value>"
}
}

The data object contains the record with RBAC field filtering applied — only fields the authenticated token is permitted to read are included.

RBAC and Event Filtering

Events are filtered per-connection based on the role associated with the authenticated token.

Field-level filtering: If the role defines a fields whitelist or excludeFields blocklist for an entity, only permitted fields are included in the data payload. This mirrors the same filtering applied to REST API responses.

Row-level filtering: If the role defines a rowFilter for an entity, events for that entity are suppressed for this connection. Full predicate evaluation against push events is not yet supported.

Limits

LimitValue
Max subscriptions per connection10
Max concurrent connections per tenant100

Exceeding either limit returns an error message — the connection is not closed.

Error Types

codeWhen
not_authenticatedMessage sent before auth, or token is invalid/expired
auth_expiredToken was valid but has since expired; re-authenticate
limit_exceededPer-connection subscription limit (10) or per-tenant connection limit (100) reached
entity_not_foundSubscribed entity does not exist in this tenant

Reconnection Strategy

Implement exponential backoff on disconnect.

AttemptWait before reconnect
11s
22s
34s
48s
516s
6+30s (max)

Formula: min(1000 * 2^(attempt - 1), 30000) ms.

On reconnect, repeat the full handshake: send auth, wait for auth_ok, then re-send all subscribe messages.

Code Examples

JavaScript / TypeScript (browser)

class FysoWebSocket {
private ws: WebSocket | null = null;
private attempt = 0;
private readonly maxBackoffMs = 30_000;
private subscriptions = new Set<string>();

constructor(
private readonly url: string,
private readonly token: string,
) {}

connect() {
this.ws = new WebSocket(this.url);

this.ws.addEventListener('open', () => {
this.attempt = 0;
this.send({ type: 'auth', token: this.token });
});

this.ws.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);

if (msg.type === 'auth_ok') {
// Re-subscribe after reconnect
for (const entity of this.subscriptions) {
this.send({ type: 'subscribe', entity });
}
} else if (msg.type === 'record.created') {
console.log('created', msg.entity, msg.data);
} else if (msg.type === 'record.updated') {
console.log('updated', msg.entity, msg.data);
} else if (msg.type === 'record.deleted') {
console.log('deleted', msg.entity, msg.data);
}
});

this.ws.addEventListener('close', () => this.scheduleReconnect());
this.ws.addEventListener('error', () => this.ws?.close());
}

subscribe(entity: string) {
this.subscriptions.add(entity);
this.send({ type: 'subscribe', entity });
}

private send(msg: object) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(msg));
}
}

private scheduleReconnect() {
const delay = Math.min(1_000 * 2 ** this.attempt, this.maxBackoffMs);
this.attempt++;
setTimeout(() => this.connect(), delay);
}
}

// Usage
const client = new FysoWebSocket('wss://api.fyso.dev/ws', 'fyso_pkey_...');
client.connect();
client.subscribe('tickets');

Node.js

const WebSocket = require('ws');

const TOKEN = 'fyso_pkey_...';
const URL = 'wss://api.fyso.dev/ws';
const ENTITIES = ['tickets', 'comments'];

let attempt = 0;

function connect() {
const ws = new WebSocket(URL);

ws.on('open', () => {
attempt = 0;
ws.send(JSON.stringify({ type: 'auth', token: TOKEN }));
});

ws.on('message', (raw) => {
const msg = JSON.parse(raw);

if (msg.type === 'auth_ok') {
for (const entity of ENTITIES) {
ws.send(JSON.stringify({ type: 'subscribe', entity }));
}
} else if (msg.type.startsWith('record.')) {
console.log(`[${msg.type}] ${msg.entity}`, msg.data);
} else if (msg.type === 'error') {
console.error('WS error:', msg.code, msg.message);
}
});

ws.on('close', () => {
const delay = Math.min(1000 * 2 ** attempt, 30_000);
attempt++;
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(connect, delay);
});

ws.on('error', (err) => {
console.error('WebSocket error:', err.message);
ws.terminate();
});

// Keepalive
const ping = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30_000);

ws.on('close', () => clearInterval(ping));
}

connect();

Python

Requires the websockets library (pip install websockets).

import asyncio
import json
import websockets

TOKEN = "fyso_pkey_..."
URL = "wss://api.fyso.dev/ws"
ENTITIES = ["tickets", "comments"]


async def connect_with_backoff():
attempt = 0
while True:
try:
async with websockets.connect(URL) as ws:
attempt = 0

await ws.send(json.dumps({"type": "auth", "token": TOKEN}))

async for raw in ws:
msg = json.loads(raw)

if msg["type"] == "auth_ok":
for entity in ENTITIES:
await ws.send(json.dumps({"type": "subscribe", "entity": entity}))

elif msg["type"].startswith("record."):
print(f"[{msg['type']}] {msg['entity']}: {msg['data']}")

elif msg["type"] == "error":
print(f"Error: {msg['code']}{msg['message']}")

except (websockets.ConnectionClosed, OSError) as exc:
delay = min(1 * 2 ** attempt, 30)
attempt += 1
print(f"Disconnected ({exc}). Reconnecting in {delay}s...")
await asyncio.sleep(delay)


asyncio.run(connect_with_backoff())

websocat CLI

Useful for quick testing.

websocat wss://api.fyso.dev/ws

Then send messages manually:

{"type":"auth","token":"fyso_pkey_..."}
{"type":"subscribe","entity":"tickets"}
{"type":"ping"}

Pipe a sequence:

(
echo '{"type":"auth","token":"fyso_pkey_..."}'
sleep 0.5
echo '{"type":"subscribe","entity":"tickets"}'
sleep 3600
) | websocat wss://api.fyso.dev/ws