Skip to main content

Overview

The WebSocket API provides real-time streaming of PostgreSQL database changes to connected clients. When you connect to the WebSocket endpoint, you’ll receive an initial snapshot of all changes and then continuous updates as new changes occur.

Connection

Endpoint

ws://localhost:3000/ws

Establishing Connection

const ws = new WebSocket("ws://localhost:3000/ws");

ws.onopen = () => {
  console.log("Connected to PostgreSQL Realtime Monitor");
};

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

ws.onerror = (error) => {
  console.error("WebSocket error:", error);
};

ws.onclose = (event) => {
  console.log("Disconnected:", event.code, event.reason);
};

Message Types

The server sends JSON messages with the following structure:

WebSocketMessage Interface

interface WebSocketMessage {
  type: "initial" | "change";
  data: Change[];
  total?: number;
}
type
'initial' | 'change'
required
The type of message being sent:
  • initial: Sent immediately after connection with all existing changes
  • change: Sent when new database changes occur
data
Change[]
required
Array of database change objects. Each change includes the operation type, table name, and all row data.
total
number
Total number of changes tracked by the server. Only included in change messages.

Change Object

interface Change {
  operation: string;
  table: string;
  [key: string]: any;
}
operation
string
required
The database operation that occurred. Possible values:
  • INSERT: New row was created
  • UPDATE: Existing row was modified
  • DELETE: Row was removed
table
string
required
The fully qualified table name in the format schema.table (e.g., public.users).
[key: string]
any
All columns from the affected row are included as additional properties. Column names become property keys with their corresponding values.

Message Examples

Initial Message

Sent immediately when a client connects, containing all previously captured changes:
{
  "type": "initial",
  "data": [
    {
      "operation": "INSERT",
      "table": "public.users",
      "id": 1,
      "name": "John Doe",
      "email": "[email protected]",
      "created_at": "2026-03-03T10:30:00Z"
    },
    {
      "operation": "UPDATE",
      "table": "public.products",
      "id": 42,
      "name": "Widget",
      "price": 29.99,
      "updated_at": "2026-03-03T11:15:00Z"
    }
  ]
}

Change Message

Sent in real-time when database operations occur:
{
  "type": "change",
  "data": [
    {
      "operation": "INSERT",
      "table": "public.orders",
      "id": 123,
      "user_id": 1,
      "amount": 99.99,
      "status": "pending",
      "created_at": "2026-03-03T12:00:00Z"
    }
  ],
  "total": 3
}

Batch Changes

Multiple changes from a single operation are sent together:
{
  "type": "change",
  "data": [
    {
      "operation": "DELETE",
      "table": "public.old_records",
      "id": 1,
      "deleted_at": "2026-03-03T12:30:00Z"
    },
    {
      "operation": "DELETE",
      "table": "public.old_records",
      "id": 2,
      "deleted_at": "2026-03-03T12:30:00Z"
    }
  ],
  "total": 5
}

Connection Lifecycle

On Connect

1

Client initiates connection

Client establishes WebSocket connection to /ws endpoint
2

Server accepts connection

Server upgrades the HTTP connection to WebSocket protocol
3

Server sends initial state

If changes exist, server immediately sends an initial message with all accumulated changes
4

Client receives real-time updates

Client now receives change messages as database operations occur

On Disconnect

  • Server removes client from broadcast list
  • Connection can be re-established at any time
  • Upon reconnection, client receives fresh initial message with all changes

Automatic Reconnection

Implement reconnection logic for robust connections:
let ws = null;
let reconnectTimeout = null;

function connect() {
  ws = new WebSocket("ws://localhost:3000/ws");
  
  ws.onopen = () => {
    console.log("Connected");
  };
  
  ws.onclose = (event) => {
    console.log("Disconnected");
    
    // Reconnect after 3 seconds if not intentional close
    if (event.code !== 1000) {
      reconnectTimeout = setTimeout(connect, 3000);
    }
  };
  
  ws.onerror = (error) => {
    console.error("Error:", error);
  };
}

connect();

Broadcasting Behavior

All connected clients receive the same messages simultaneously. Changes are broadcast to every active WebSocket connection.

Server Implementation

The server maintains a set of connected clients and broadcasts messages:
const clients = new Set<any>();

function broadcast(data: any) {
  const message = JSON.stringify(data);
  clients.forEach((client: any) => {
    if (client.readyState === 1) { // WebSocket.OPEN
      client.send(message);
    }
  });
}

Client Message Handling

Clients can send messages to the server, though the current implementation primarily logs them:
// Send a message to the server
ws.send("ping");

// Server logs received messages
// Message received: ping
The WebSocket protocol supports bidirectional communication, but the primary data flow is server-to-client for database change notifications.

Error Handling

Connection Errors

ws.onerror = (error) => {
  console.error("WebSocket error:", error);
  // Handle connection issues
  // Typically followed by onclose event
};

Message Parsing Errors

ws.onmessage = (event) => {
  try {
    const message = JSON.parse(event.data);
    // Process message
  } catch (error) {
    console.error("Error parsing message:", error);
  }
};

Close Codes

1000
Normal Closure
Normal closure, typically when client intentionally disconnects or component unmounts.
1001
Going Away
Browser tab closed or navigating away from page.
1006
Abnormal Closure
Connection lost unexpectedly (network issues, server crash).

Best Practices

Implement Reconnection

Always include automatic reconnection logic with exponential backoff to handle network interruptions gracefully.

Parse Messages Safely

Wrap JSON.parse() in try-catch blocks to handle malformed messages without crashing your application.

Track Connection State

Monitor readyState and connection events to update your UI and inform users of connection status.

Clean Up on Unmount

Always close WebSocket connections when components unmount to prevent memory leaks and ghost connections.

TypeScript Usage

Full type-safe WebSocket implementation:
import { useState, useEffect, useRef } from "react";

interface Change {
  operation: string;
  table: string;
  [key: string]: any;
}

interface WebSocketMessage {
  type: "initial" | "change";
  data: Change[];
  total?: number;
}

function useRealtimeChanges() {
  const [changes, setChanges] = useState<Change[]>([]);
  const [connected, setConnected] = useState(false);
  const wsRef = useRef<WebSocket | null>(null);

  useEffect(() => {
    const ws = new WebSocket("ws://localhost:3000/ws");
    wsRef.current = ws;

    ws.onopen = () => setConnected(true);

    ws.onmessage = (event) => {
      const message: WebSocketMessage = JSON.parse(event.data);

      if (message.type === "initial") {
        setChanges(message.data);
      } else if (message.type === "change") {
        setChanges((prev) => [...prev, ...message.data]);
      }
    };

    ws.onerror = () => setConnected(false);
    ws.onclose = () => setConnected(false);

    return () => {
      ws.close(1000, "Component unmounting");
    };
  }, []);

  return { changes, connected };
}

Build docs developers (and LLMs) love