Pular para o conteúdo principal

Real-Time

ProBeya provides real-time updates so all users viewing the same board see changes instantly. This is implemented using WebSocket connections with Redis PubSub as the message broker.

Architecture​

Next.js web -- publishes board updates --> Redis PubSub
|
Client A -- authenticated WebSocket --> ws --+-- room fan-out
Client B -- authenticated WebSocket --> ws --+
  1. Next.js verifies the user's tenant, membership, board, and view permission, then mints a short-lived board-scoped token.
  2. The client presents that token as a WebSocket subprotocol during the HTTP upgrade; identity is never accepted from a query parameter or message body.
  3. The Node.js ws service subscribes to Redis channels and pushes authorized updates to board rooms across instances.
  4. Clients apply the update to their local state.

Connecting​

Using Raw WebSocket​

const boardId = "brd_abc123";
const tokenResponse = await fetch("/api/ws/token", {
method: "POST",
credentials: "same-origin",
cache: "no-store",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ boardId }),
});

if (!tokenResponse.ok) throw new Error("Board subscription denied");
const { token } = await tokenResponse.json();

// NEXT_PUBLIC_WS_URL in production, ws://localhost:8003 locally.
const ws = new WebSocket("wss://ws.probeya.com", ["probeya-v1", token]);

ws.onopen = () => {
ws.send(JSON.stringify({ type: "join_board", boardId }));
};

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("Event:", data);
};

The token is intentionally kept out of the URL so reverse-proxy access logs do not capture it. Reconnects must request a fresh token and therefore re-run ACL checks.

Event Types​

Board Events​

interface BoardUpdateEvent {
type: "board_update";
action: string;
data: Record<string, unknown>;
}

Item Events​

type ItemAction =
| "item_created"
| "item_updated"
| "item_deleted"
| "item_moved";

Presence Events​

interface PresenceEvent {
type: "presence";
boardId: string;
users: {
id: string;
name: string;
avatar?: string;
}[];
}

Client Messages​

MessageDescription
{ "type": "join_board", "boardId": "..." }Join the one board authorized by the handshake token
{ "type": "leave_board", "boardId": "..." }Leave a board room
{ "type": "ping" }Keepalive request

Optimistic Updates​

The client applies mutations locally before the server confirms them. When the server event arrives, the client reconciles:

// 1. Apply optimistic update locally
updateLocalState({ itemId: "itm_abc123", status: "done" });

// 2. Send mutation to server
await client.item.update({ id: "itm_abc123", fields: { status: "done" } });

// 3. Server broadcasts event to all clients (including sender)
// 4. Client recognizes its own event and skips re-applying

This ensures the UI feels instant while maintaining consistency across all clients.

Connection Management​

  • Connections automatically reconnect on disconnect with exponential backoff.
  • Heartbeat pings are sent every 30 seconds to detect stale connections.
  • The server closes idle connections after 5 minutes of no subscriptions.