WebSockets & Real-Time Apps
Real-Time with GraphQL Subscriptions
Introduction to GraphQL Subscriptions
GraphQL subscriptions provide a way to push real-time updates from the server to clients when specific events occur. Unlike queries (request data) and mutations (modify data), subscriptions maintain an active connection and stream updates.
What are GraphQL Subscriptions?
Subscriptions are a GraphQL feature that allows:
- Real-time Updates: Push data to clients as events happen
- Event-Driven: Clients subscribe to specific events
- WebSocket-Based: Uses WebSocket protocol for bidirectional communication
- Type-Safe: Full GraphQL type system support
Key Concept: Subscriptions complete the GraphQL triad: Queries (read), Mutations (write), Subscriptions (real-time updates).
Why Use GraphQL Subscriptions?
Benefits over traditional REST WebSockets:
- Type-safe real-time data with GraphQL schema
- Request exactly the data you need (no over-fetching)
- Built-in filtering and arguments
- Unified API surface (queries, mutations, subscriptions in one endpoint)
- Strong tooling and client support (Apollo, Relay, URQL)
GraphQL Subscription Schema
Define subscriptions in your GraphQL schema:
// schema.graphql
type Message {
id: ID!
content: String!
author: User!
createdAt: String!
}
type User {
id: ID!
username: String!
avatar: String
}
type Query {
messages: [Message!]!
}
type Mutation {
sendMessage(content: String!): Message!
}
type Subscription {
# Subscribe to new messages in a room
messageAdded(roomId: ID!): Message!
# Subscribe to typing indicators
userTyping(roomId: ID!): User!
# Subscribe to user status changes
userStatusChanged(userId: ID!): UserStatus!
}
enum UserStatus {
ONLINE
OFFLINE
AWAY
}
Apollo Server Setup
Set up Apollo Server with subscription support:
// Install dependencies
// npm install apollo-server-express graphql subscriptions-transport-ws
const { ApolloServer, gql } = require('apollo-server-express');
const { PubSub } = require('graphql-subscriptions');
const { createServer } = require('http');
const express = require('express');
const { ApolloServerPluginDrainHttpServer } = require('apollo-server-core');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const app = express();
const httpServer = createServer(app);
// PubSub instance for publishing/subscribing to events
const pubsub = new PubSub();
// GraphQL schema
const typeDefs = gql`
type Message {
id: ID!
content: String!
author: String!
timestamp: String!
}
type Query {
messages: [Message!]!
}
type Mutation {
sendMessage(content: String!, author: String!): Message!
}
type Subscription {
messageAdded: Message!
}
`;
// Resolvers
const resolvers = {
Query: {
messages: () => messages
},
Mutation: {
sendMessage: (_, { content, author }) => {
const message = {
id: String(Date.now()),
content,
author,
timestamp: new Date().toISOString()
};
messages.push(message);
// Publish event to subscribers
pubsub.publish('MESSAGE_ADDED', {
messageAdded: message
});
return message;
}
},
Subscription: {
messageAdded: {
subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED'])
}
}
};
// Create schema
const schema = makeExecutableSchema({ typeDefs, resolvers });
// WebSocket server for subscriptions
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql'
});
const serverCleanup = useServer({ schema }, wsServer);
// Apollo Server
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
}
};
}
}
]
});
await server.start();
server.applyMiddleware({ app });
httpServer.listen(4000, () => {
console.log(`Server ready at http://localhost:4000${server.graphqlPath}`);
console.log(`Subscriptions ready at ws://localhost:4000${server.graphqlPath}`);
});
Important: Subscriptions require both HTTP server (for queries/mutations) and WebSocket server (for subscriptions) running on the same port.
Subscription Resolvers
Subscription resolvers use async iterators:
const resolvers = {
Subscription: {
messageAdded: {
// Simple subscription
subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED'])
},
// Subscription with filtering
messageAddedInRoom: {
subscribe: (_, { roomId }) => {
return pubsub.asyncIterator([`MESSAGE_ADDED_${roomId}`]);
}
},
// Subscription with custom filtering
messageAddedForUser: {
subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED']),
resolve: (payload, args, context) => {
// Filter messages based on user permissions
if (payload.messageAdded.private &&
payload.messageAdded.recipientId !== context.userId) {
return null; // Don't send to this subscriber
}
return payload.messageAdded;
}
}
}
};
// Publishing events
const sendMessage = async (roomId, message) => {
await db.messages.create(message);
// Publish to room-specific channel
pubsub.publish(`MESSAGE_ADDED_${roomId}`, {
messageAddedInRoom: message
});
};
PubSub for Event Publishing
The PubSub pattern connects mutations to subscriptions:
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
// Event names as constants
const EVENTS = {
MESSAGE_ADDED: 'MESSAGE_ADDED',
USER_TYPING: 'USER_TYPING',
USER_STATUS_CHANGED: 'USER_STATUS_CHANGED'
};
const resolvers = {
Mutation: {
sendMessage: async (_, { roomId, content }, { userId }) => {
const message = await db.messages.create({
roomId,
content,
authorId: userId
});
// Publish to all subscribers
await pubsub.publish(EVENTS.MESSAGE_ADDED, {
messageAdded: message,
roomId // Can be used for filtering
});
return message;
},
setTyping: async (_, { roomId, isTyping }, { userId }) => {
await pubsub.publish(EVENTS.USER_TYPING, {
userTyping: {
userId,
roomId,
isTyping
}
});
return { success: true };
}
},
Subscription: {
messageAdded: {
subscribe: (_, { roomId }) => {
return pubsub.asyncIterator([EVENTS.MESSAGE_ADDED]);
},
resolve: (payload, { roomId }) => {
// Filter by room
if (payload.roomId === roomId) {
return payload.messageAdded;
}
return null;
}
},
userTyping: {
subscribe: (_, { roomId }) => {
return pubsub.asyncIterator([EVENTS.USER_TYPING]);
},
resolve: (payload, { roomId }) => {
if (payload.userTyping.roomId === roomId) {
return payload.userTyping;
}
return null;
}
}
}
};
Best Practice: Use constants for event names to avoid typos and make refactoring easier.
Filtering Subscriptions
Filter which subscribers receive events:
// Method 1: Channel-based filtering (most efficient)
const resolvers = {
Subscription: {
messageAdded: {
subscribe: (_, { roomId }) => {
// Each room has its own channel
return pubsub.asyncIterator([`ROOM_${roomId}_MESSAGES`]);
}
}
},
Mutation: {
sendMessage: async (_, { roomId, content }) => {
const message = await saveMessage(roomId, content);
// Publish to room-specific channel
pubsub.publish(`ROOM_${roomId}_MESSAGES`, {
messageAdded: message
});
return message;
}
}
};
// Method 2: Resolver-based filtering (more flexible)
const resolvers = {
Subscription: {
messageAdded: {
subscribe: () => pubsub.asyncIterator(['ALL_MESSAGES']),
resolve: (payload, args, context) => {
const { messageAdded } = payload;
const { roomId } = args;
const { user } = context;
// Only send if user is in the room
if (messageAdded.roomId !== roomId) return null;
// Only send if user has permission
if (!user.hasAccessToRoom(roomId)) return null;
return messageAdded;
}
}
}
};
// Method 3: withFilter helper (Apollo provides this)
const { withFilter } = require('graphql-subscriptions');
const resolvers = {
Subscription: {
messageAdded: {
subscribe: withFilter(
() => pubsub.asyncIterator(['MESSAGE_ADDED']),
(payload, variables, context) => {
// Return true to send, false to skip
return payload.messageAdded.roomId === variables.roomId &&
context.user.hasAccessToRoom(variables.roomId);
}
)
}
}
};
Client-Side Subscription Handling
Connect and subscribe from a React client:
// Install dependencies
// npm install @apollo/client graphql subscriptions-transport-ws
import { ApolloClient, InMemoryCache, HttpLink, split } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';
// HTTP connection for queries and mutations
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql'
});
// WebSocket connection for subscriptions
const wsLink = new GraphQLWsLink(createClient({
url: 'ws://localhost:4000/graphql'
}));
// Split traffic between HTTP and WebSocket
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink
);
// Apollo Client
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache()
});
Using Subscriptions in React
import { useSubscription, gql, useMutation } from '@apollo/client';
const MESSAGE_ADDED = gql`
subscription MessageAdded($roomId: ID!) {
messageAdded(roomId: $roomId) {
id
content
author {
id
username
avatar
}
createdAt
}
}
`;
const SEND_MESSAGE = gql`
mutation SendMessage($roomId: ID!, $content: String!) {
sendMessage(roomId: $roomId, content: $content) {
id
content
author {
id
username
}
createdAt
}
}
`;
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
// Subscribe to new messages
const { data, loading, error } = useSubscription(MESSAGE_ADDED, {
variables: { roomId },
onSubscriptionData: ({ subscriptionData }) => {
const newMessage = subscriptionData.data.messageAdded;
setMessages(prev => [...prev, newMessage]);
}
});
// Mutation to send messages
const [sendMessage] = useMutation(SEND_MESSAGE);
const handleSend = async (content) => {
await sendMessage({
variables: { roomId, content }
});
};
return (
<div>
{messages.map(msg => (
<div key={msg.id}>
<strong>{msg.author.username}:</strong> {msg.content}
</div>
))}
<MessageInput onSend={handleSend} />
</div>
);
}
Exercise:
- Set up an Apollo Server with GraphQL subscriptions
- Create a schema with:
- Query for fetching initial data
- Mutation for creating posts
- Subscription for receiving new posts
- Implement filtering so users only receive posts from followed users
- Create a React client with Apollo Client
- Subscribe to real-time updates and display them in the UI
- Test with multiple browser windows to verify real-time sync
Summary
GraphQL subscriptions provide type-safe real-time communication:
- WebSocket-based bidirectional communication
- Type-safe with full GraphQL schema support
- Request exactly the data you need (no over-fetching)
- Built-in filtering with arguments and resolvers
- Unified API with queries and mutations
- Strong client support with Apollo, Relay, URQL