GraphQL

Subscriptions

20 min Lesson 16 of 35

Real-time Updates with GraphQL Subscriptions

Subscriptions enable real-time, event-based communication between server and client. Unlike queries and mutations, subscriptions maintain a persistent connection and push data when events occur.

What are Subscriptions?

Subscriptions allow clients to listen to real-time updates from the server. They're ideal for chat applications, live notifications, real-time dashboards, and collaborative editing.

Subscription Schema Definition:
type Subscription {
  messageAdded(channelId: ID!): Message
  userStatusChanged(userId: ID!): User
  commentAdded(postId: ID!): Comment
  notificationReceived: Notification
}

type Message {
  id: ID!
  content: String!
  author: User!
  channel: Channel!
  createdAt: DateTime!
}

PubSub Pattern

GraphQL subscriptions typically use the Publish-Subscribe pattern. The server publishes events to topics, and subscribed clients receive updates.

PubSub Implementation (Node.js with Apollo):
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

// Mutation that publishes event
const resolvers = {
  Mutation: {
    sendMessage: async (parent, { channelId, content }, { user }) => {
      const message = await Message.create({
        channelId,
        content,
        authorId: user.id
      });

      // Publish event to subscribers
      pubsub.publish('MESSAGE_ADDED', {
        messageAdded: message,
        channelId
      });

      return message;
    }
  },

  Subscription: {
    messageAdded: {
      subscribe: (parent, { channelId }) => {
        return pubsub.asyncIterator(['MESSAGE_ADDED']);
      },
      resolve: (payload, { channelId }) => {
        // Filter by channel
        if (payload.channelId === channelId) {
          return payload.messageAdded;
        }
        return null;
      }
    }
  }
};
Note: The in-memory PubSub is suitable for development but not production. For production, use Redis PubSub or similar distributed systems to handle multiple server instances.

Subscription Resolvers

Subscription resolvers have two functions: subscribe (sets up the subscription) and resolve (transforms the published data).

Advanced Subscription Resolver:
const resolvers = {
  Subscription: {
    commentAdded: {
      // Setup subscription
      subscribe: withFilter(
        () => pubsub.asyncIterator(['COMMENT_ADDED']),
        (payload, variables) => {
          // Filter: only send if comment is for this post
          return payload.commentAdded.postId === variables.postId;
        }
      ),

      // Transform payload
      resolve: (payload) => {
        return payload.commentAdded;
      }
    },

    userStatusChanged: {
      subscribe: withFilter(
        () => pubsub.asyncIterator(['USER_STATUS_CHANGED']),
        (payload, variables, context) => {
          // Auth check
          if (!context.user) return false;

          // Only notify friends
          return payload.userId === variables.userId &&
                 context.user.friends.includes(payload.userId);
        }
      )
    }
  }
};

WebSocket Transport

Subscriptions require WebSocket connections for bidirectional communication. Apollo Server uses the graphql-ws protocol.

Apollo Server Setup with Subscriptions:
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import express from 'express';

const app = express();
const httpServer = createServer(app);

const schema = makeExecutableSchema({
  typeDefs,
  resolvers
});

// WebSocket server for subscriptions
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql'
});

const serverCleanup = useServer({ schema }, wsServer);

const server = new ApolloServer({
  schema,
  plugins: [
    ApolloServerPluginDrainHttpServer({ httpServer }),
    {
      async serverWillStart() {
        return {
          async drainServer() {
            await serverCleanup.dispose();
          }
        };
      }
    }
  ]
});

await server.start();
app.use('/graphql', express.json(), expressMiddleware(server));

httpServer.listen(4000);

Filtering Subscriptions

The withFilter function allows you to filter which subscribers receive which events.

Filtering Examples:
import { withFilter } from 'graphql-subscriptions';

const resolvers = {
  Subscription: {
    // Filter by user ID
    notificationReceived: {
      subscribe: withFilter(
        () => pubsub.asyncIterator(['NOTIFICATION']),
        (payload, variables) => {
          return payload.userId === variables.userId;
        }
      )
    },

    // Filter by multiple criteria
    orderUpdated: {
      subscribe: withFilter(
        () => pubsub.asyncIterator(['ORDER_UPDATED']),
        (payload, variables, context) => {
          const { orderId, status } = variables;
          const order = payload.orderUpdated;

          // Filter by order ID
          if (orderId && order.id !== orderId) return false;

          // Filter by status
          if (status && order.status !== status) return false;

          // Auth: user must own the order
          return order.userId === context.user.id;
        }
      )
    }
  }
};

Subscription Lifecycle

Understanding the subscription lifecycle helps debug and optimize real-time features.

Lifecycle Hooks:
const resolvers = {
  Subscription: {
    messageAdded: {
      subscribe: async (parent, args, context) => {
        // 1. Connection established
        console.log('Subscriber connected:', context.user.id);

        // 2. Validate permissions
        const channel = await Channel.findById(args.channelId);
        if (!channel.hasAccess(context.user)) {
          throw new Error('Access denied');
        }

        // 3. Setup iterator
        const iterator = pubsub.asyncIterator(['MESSAGE_ADDED']);

        // 4. Cleanup on disconnect
        const cleanup = () => {
          console.log('Subscriber disconnected:', context.user.id);
          // Cleanup resources
        };

        iterator.return = async () => {
          cleanup();
          return { value: undefined, done: true };
        };

        return iterator;
      }
    }
  }
};
Tip: Monitor active subscriptions and implement cleanup to prevent memory leaks. Consider limiting the number of concurrent subscriptions per user.

Client-Side Subscription (React)

React Subscription Hook:
import { useSubscription, gql } from '@apollo/client';

const MESSAGE_SUBSCRIPTION = gql`
  subscription MessageAdded($channelId: ID!) {
    messageAdded(channelId: $channelId) {
      id
      content
      author {
        id
        name
      }
      createdAt
    }
  }
`;

function ChatRoom({ channelId }) {
  const { data, loading, error } = useSubscription(
    MESSAGE_SUBSCRIPTION,
    { variables: { channelId } }
  );

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

  return (
    <div>
      {data && (
        <div className="new-message">
          <strong>{data.messageAdded.author.name}:</strong>
          {data.messageAdded.content}
        </div>
      )}
    </div>
  );
}
Warning: Subscriptions consume server resources. Implement rate limiting, authentication, and consider using Redis PubSub for horizontal scaling across multiple server instances.
Exercise:
  1. Implement a subscription for live comment updates on a blog post
  2. Add filtering to only send comments to users who have access to the post
  3. Create a mutation that publishes the comment event
  4. Build a React component that subscribes and displays new comments in real-time
  5. Add authentication to the subscription context