subscriptions
graphql

Real-Time Subscriptions

Excalibase GraphQL exposes real-time table changes as GraphQL subscriptions, powered by excalibase-watcher and NATS JetStream.

Works with PostgreSQL and MySQL. The watcher captures changes via logical replication (PostgreSQL) or binlog (MySQL) and publishes them to NATS.

How it works

Database change (INSERT / UPDATE / DELETE)
        ↓
  WAL (PostgreSQL) or Binlog (MySQL)
        ↓
  excalibase-watcher (CDC server)
        ↓
  NATS JetStream (cdc.{schema}.{table})
        ↓
  excalibase-graphql (NatsCDCService)
        ↓
  GraphQL subscription (WebSocket)
        ↓
      Client

The watcher owns the replication slot / binlog connection. Excalibase-graphql is a pure NATS consumer — this means you can scale horizontally without duplicate CDC events.

DDL changes (CREATE TABLE, ALTER TABLE, etc.) are also captured and automatically invalidate the GraphQL schema cache — no restart needed.

Setup

1. Enable logical replication (PostgreSQL)

-- postgresql.conf
wal_level = logical
max_replication_slots = 5
max_wal_senders = 5

The provided docker-compose.yml already has this configured, along with NATS and excalibase-watcher.

2. Configure NATS connection

app:
  nats:
    enabled: true
    url: nats://localhost:4222
    stream-name: CDC
    subject-prefix: cdc

3. Subscribe to changes

subscription {
  customerChanges {
    operation   # INSERT | UPDATE | DELETE | ERROR
    table
    timestamp
    data {
      customer_id
      first_name
      last_name
      email
      new {       # populated on UPDATE
        customer_id
        first_name
        last_name
        email
      }
    }
    error
  }
}

Operation types

INSERT — full row data

{
  "operation": "INSERT",
  "data": {
    "customer_id": 13,
    "first_name": "John",
    "last_name": "Doe",
    "email": "john@example.com"
  }
}

UPDATE — new values nested under new

{
  "operation": "UPDATE",
  "data": {
    "new": {
      "customer_id": 13,
      "first_name": "Jane",
      "last_name": "Doe",
      "email": "jane@example.com"
    }
  }
}

DELETE — primary key only (REPLICA IDENTITY DEFAULT)

{
  "operation": "DELETE",
  "data": {
    "customer_id": 13
  }
}

REPLICA IDENTITY

Controls how much data appears in UPDATE/DELETE events:

-- Default: UPDATE/DELETE only include primary key
ALTER TABLE customer REPLICA IDENTITY DEFAULT;

-- Full: UPDATE/DELETE include all column values
ALTER TABLE customer REPLICA IDENTITY FULL;

Recommendation: use REPLICA IDENTITY FULL for tables where clients need the full row on DELETE or before/after values on UPDATE.

WebSocket endpoint

Subscriptions connect via WebSocket to:

ws://localhost:10000/graphql

Protocol: graphql-transport-ws. Most GraphQL clients (Apollo Client, urql, graphql-ws) support this automatically.

Horizontal scaling

Because excalibase-watcher owns the single replication slot and publishes to NATS, you can run multiple excalibase-graphql pods behind a load balancer. Each pod receives every event via NATS fan-out — no duplicate replication slots, no duplicate events.

Related