Skip to main content

Overview

GraphQL subscriptions enable real-time communication in RealtimeChat. When messages are sent, edited, or deleted, all subscribed clients receive instant updates via WebSocket.
Subscriptions require a WebSocket connection to the GraphQL endpoint. Standard HTTP requests will not work.

How Subscriptions Work

  1. Client connects to the GraphQL WebSocket endpoint
  2. Client subscribes to specific events (e.g., OnMessageUpdated)
  3. Server pushes updates whenever mutations trigger events
  4. Client receives real-time notifications without polling

Event-Driven

Receive updates only when changes occur

Low Latency

Near-instant message delivery

Efficient

No need for polling or repeated queries

Bi-directional

Full-duplex WebSocket communication

WebSocket Endpoint

Connect to the GraphQL subscription endpoint:
ws://localhost:5000/graphql
For production (with TLS):
wss://your-domain.com/graphql

OnMessageUpdated

Subscribe to all message events (added, updated, deleted) across the chat system.

Event Types

The subscription emits events with three possible types:
ADDED
String
A new message was sent to a chat room
UPDATED
String
An existing message was edited
DELETED
String
A message was deleted from a chat room

GraphQL Schema

type Subscription {
  onMessageUpdated: MessageUpdatedEvent!
}

type MessageUpdatedEvent {
  eventType: String!
  message: MessageGraph
}

type MessageGraph {
  id: Int!
  sentAt: DateTime!
  senderId: String!
  chatRoomId: Int!
  content: MessageContent!
}

Example Subscription

subscription OnMessageUpdated {
  onMessageUpdated {
    eventType
    message {
      id
      senderId
      sentAt
      chatRoomId
      content {
        ... on TextMessageContentGraph {
          text
        }
      }
    }
  }
}

Event Payloads

ADDED Event

Received when a new message is sent via sendMessage mutation:
{
  "data": {
    "onMessageUpdated": {
      "eventType": "ADDED",
      "message": {
        "id": 105,
        "senderId": "user456",
        "sentAt": "2026-03-05T11:00:00Z",
        "chatRoomId": 1,
        "content": {
          "text": "New message just arrived!"
        }
      }
    }
  }
}

UPDATED Event

Received when a message is edited via editMessage mutation:
{
  "data": {
    "onMessageUpdated": {
      "eventType": "UPDATED",
      "message": {
        "id": 105,
        "senderId": "user456",
        "sentAt": "2026-03-05T11:00:00Z",
        "chatRoomId": 1,
        "content": {
          "text": "New message just arrived! (edited)"
        }
      }
    }
  }
}

DELETED Event

Received when a message is deleted via deleteMessage mutation:
{
  "data": {
    "onMessageUpdated": {
      "eventType": "DELETED",
      "message": {
        "id": 105,
        "senderId": "user456",
        "sentAt": "2026-03-05T11:00:00Z",
        "chatRoomId": 1,
        "content": {
          "text": "New message just arrived! (edited)"
        }
      }
    }
  }
}

Implementation Details

From ChatRoomSubscription.cs:3-8:
public class MessageSubscription
{
    [Subscribe]
    [Topic("MessageUpdated")]
    public MessageUpdatedEvent OnMessageUpdated(
        [EventMessage] MessageUpdatedEvent @event
    ) => @event;
}
The subscription listens to the "MessageUpdated" topic, which is published by all message mutations.

Client Implementation

JavaScript/TypeScript Client

Using graphql-ws library:
import { createClient } from 'graphql-ws';

const client = createClient({
  url: 'ws://localhost:5000/graphql',
});

const subscription = client.subscribe(
  {
    query: `
      subscription {
        onMessageUpdated {
          eventType
          message {
            id
            senderId
            sentAt
            chatRoomId
            content {
              ... on TextMessageContentGraph {
                text
              }
            }
          }
        }
      }
    `,
  },
  {
    next: (data) => {
      const { eventType, message } = data.data.onMessageUpdated;
      
      switch (eventType) {
        case 'ADDED':
          console.log('New message:', message);
          // Add message to UI
          break;
        case 'UPDATED':
          console.log('Message updated:', message);
          // Update message in UI
          break;
        case 'DELETED':
          console.log('Message deleted:', message);
          // Remove message from UI
          break;
      }
    },
    error: (error) => {
      console.error('Subscription error:', error);
    },
    complete: () => {
      console.log('Subscription completed');
    },
  }
);

// Unsubscribe when done
subscription.unsubscribe();

React Hook Example

import { useEffect, useState } from 'react';
import { createClient } from 'graphql-ws';

function useMessageSubscription(chatRoomId: number) {
  const [messages, setMessages] = useState<Message[]>([]);

  useEffect(() => {
    const client = createClient({
      url: 'ws://localhost:5000/graphql',
    });

    const subscription = client.subscribe(
      {
        query: `
          subscription {
            onMessageUpdated {
              eventType
              message {
                id
                senderId
                sentAt
                chatRoomId
                content {
                  ... on TextMessageContentGraph {
                    text
                  }
                }
              }
            }
          }
        `,
      },
      {
        next: (data) => {
          const { eventType, message } = data.data.onMessageUpdated;
          
          // Only process messages from this chat room
          if (message.chatRoomId !== chatRoomId) return;

          setMessages((prev) => {
            switch (eventType) {
              case 'ADDED':
                return [...prev, message];
              case 'UPDATED':
                return prev.map((m) => 
                  m.id === message.id ? message : m
                );
              case 'DELETED':
                return prev.filter((m) => m.id !== message.id);
              default:
                return prev;
            }
          });
        },
        error: (error) => console.error(error),
      }
    );

    return () => subscription.unsubscribe();
  }, [chatRoomId]);

  return messages;
}

Apollo Client Example

import { useSubscription, gql } from '@apollo/client';

const MESSAGE_UPDATED_SUBSCRIPTION = gql`
  subscription OnMessageUpdated {
    onMessageUpdated {
      eventType
      message {
        id
        senderId
        sentAt
        chatRoomId
        content {
          ... on TextMessageContentGraph {
            text
          }
        }
      }
    }
  }
`;

function ChatRoom({ roomId }) {
  const { data, loading, error } = useSubscription(
    MESSAGE_UPDATED_SUBSCRIPTION
  );

  useEffect(() => {
    if (data?.onMessageUpdated) {
      const { eventType, message } = data.onMessageUpdated;
      
      if (message.chatRoomId === roomId) {
        handleMessageEvent(eventType, message);
      }
    }
  }, [data, roomId]);

  if (loading) return <p>Connecting...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return <div>{/* Your chat UI */}</div>;
}

Connection Lifecycle

Connection Flow

  1. Initialize WebSocket connection
  2. Send connection init message
  3. Subscribe to events
  4. Receive real-time updates
  5. Unsubscribe when done
  6. Close connection

Reconnection Strategy

import { createClient } from 'graphql-ws';

const client = createClient({
  url: 'ws://localhost:5000/graphql',
  retryAttempts: 5,
  retryWait: async (retries) => {
    // Exponential backoff: 1s, 2s, 4s, 8s, 16s
    await new Promise((resolve) => 
      setTimeout(resolve, Math.min(1000 * Math.pow(2, retries), 30000))
    );
  },
  on: {
    connected: () => console.log('Connected to GraphQL subscriptions'),
    closed: () => console.log('Connection closed'),
    error: (error) => console.error('WebSocket error:', error),
  },
});

Real-Time Chat Example

Complete example combining queries, mutations, and subscriptions:
import { createClient } from 'graphql-ws';

class ChatClient {
  private client: Client;
  private subscription: any;

  constructor(private url: string) {
    this.client = createClient({ url });
  }

  // Subscribe to message updates
  subscribeToMessages(onUpdate: (event: MessageUpdatedEvent) => void) {
    this.subscription = this.client.subscribe(
      {
        query: `
          subscription {
            onMessageUpdated {
              eventType
              message {
                id
                senderId
                sentAt
                chatRoomId
                content {
                  ... on TextMessageContentGraph {
                    text
                  }
                }
              }
            }
          }
        `,
      },
      {
        next: (data) => onUpdate(data.data.onMessageUpdated),
        error: (err) => console.error('Subscription error:', err),
      }
    );
  }

  // Send a message (mutation)
  async sendMessage(chatRoomId: number, senderId: string, text: string) {
    const response = await fetch('http://localhost:5000/graphql', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        query: `
          mutation {
            sendMessage(
              chatRoomId: ${chatRoomId}
              senderId: "${senderId}"
              text: "${text}"
            ) {
              id
              sentAt
            }
          }
        `,
      }),
    });
    return response.json();
  }

  // Fetch message history (query)
  async getMessages(chatRoomId: number) {
    const response = await fetch('http://localhost:5000/graphql', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        query: `
          query {
            getMessages(chatRoomId: ${chatRoomId}) {
              id
              senderId
              sentAt
              content {
                ... on TextMessageContentGraph {
                  text
                }
              }
            }
          }
        `,
      }),
    });
    return response.json();
  }

  disconnect() {
    this.subscription?.unsubscribe();
    this.client.dispose();
  }
}

// Usage
const chat = new ChatClient('ws://localhost:5000/graphql');

// Subscribe to updates
chat.subscribeToMessages((event) => {
  console.log(`Event: ${event.eventType}`, event.message);
});

// Load message history
const history = await chat.getMessages(1);

// Send a message
await chat.sendMessage(1, 'user123', 'Hello from the client!');

// Clean up
chat.disconnect();

Error Handling

Connection Errors

const client = createClient({
  url: 'ws://localhost:5000/graphql',
  on: {
    error: (error) => {
      console.error('WebSocket error:', error);
      // Handle connection errors
    },
    closed: (event) => {
      console.log('Connection closed:', event);
      // Handle disconnection
    },
  },
});

Subscription Errors

const subscription = client.subscribe(
  { query: '...' },
  {
    next: (data) => console.log(data),
    error: (error) => {
      console.error('Subscription error:', error);
      // Handle subscription-specific errors
    },
    complete: () => {
      console.log('Subscription completed');
      // Handle completion
    },
  }
);

Best Practices

Filter on Client

Since subscriptions receive all message events, filter by chatRoomId on the client side to display only relevant messages.

Handle All Event Types

Always handle ADDED, UPDATED, and DELETED events to keep your UI in sync.

Implement Reconnection

Use exponential backoff for reconnection attempts to handle network issues gracefully.

Unsubscribe on Unmount

Always clean up subscriptions when components unmount to prevent memory leaks.

Combine with Queries

Load initial data with queries, then use subscriptions for real-time updates.

Source Code Reference

The subscription is implemented in:
  • Infrastructure/RealtimeChat.Infrastructure.GraphQL/Subscriptions/ChatRoomSubscription.cs
Related types:
  • Infrastructure/RealtimeChat.Infrastructure.GraphQL/Events/MessageUpdatedEvent.cs
  • Infrastructure/RealtimeChat.Infrastructure.GraphQL/Models/MessageGraph.cs
Mutations that trigger events:
  • Infrastructure/RealtimeChat.Infrastructure.GraphQL/Mutations/MessageMutation.cs

Next Steps

Queries

Fetch initial message history

Mutations

Learn about mutations that trigger events

Build docs developers (and LLMs) love