Docs
Launch GraphOS Studio

Subscriptions in Apollo Server

Persistent GraphQL read operations


Apollo Server does not provide built-in support for subscriptions. You can enable support for as described below.

This article uses the graphql-ws library to add support for to 4. We no longer recommend using the previously subscriptions-transport-ws, because this library is not actively maintained. For more information about the differences between the two libraries, see Switching from subscriptions-transport-ws.

Subscriptions are long-lasting read that can update their result whenever a particular server-side event occurs. Most commonly, updated results are pushed from the server to subscribing clients. For example, a chat application's server might use a to push newly received messages to all clients in a particular chat room.

Because updates are usually pushed by the server (instead of polled by the client), they generally use the WebSocket protocol instead of HTTP.

Important: Compared to queries and , are significantly more complex to implement. Before you begin, confirm that your use case requires subscriptions.

Schema definition

Your schema's Subscription type defines top-level that clients can subscribe to:

type Subscription {
postCreated: Post
}

The postCreated will update its value whenever a new Post is created on the backend, thus pushing the Post to subscribing clients.

Clients can subscribe to the postCreated with a string, like this:

subscription PostFeed {
postCreated {
author
comment
}
}

Each can subscribe to only one of the Subscription type.

Enabling subscriptions

are not supported by 4's startStandaloneServer function. To enable , you must first swap to using the expressMiddleware function (or any other integration package that supports ).

The following steps assume you've already swapped to expressMiddleware.

To run both an Express app and a separate WebSocket server for , we'll create an http.Server instance that effectively wraps the two and becomes our new listener.

  1. Install graphql-ws, ws, and @graphql-tools/schema:

    npm install graphql-ws ws @graphql-tools/schema
  2. Add the following imports to the file where you initialize your ApolloServer instance (we'll use these in later steps):

    index.ts
    import { createServer } from 'http';
    import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
    import { makeExecutableSchema } from '@graphql-tools/schema';
    import { WebSocketServer } from 'ws';
    import { useServer } from 'graphql-ws/lib/use/ws';
  3. Next, in order to set up both the HTTP and servers, we need to first create an http.Server. Do this by passing your Express app to the createServer function, which we imported from the http module:

    index.ts
    // This `app` is the returned value from `express()`.
    const httpServer = createServer(app);
  4. Create an instance of GraphQLSchema (if you haven't already).

    If you already pass the schema option to the ApolloServer constructor (instead of typeDefs and resolvers), you can skip this step.

    The server (which we'll instantiate next) doesn't take typeDefs and resolvers options. Instead, it takes an executable GraphQLSchema. We can pass this schema object to both the server and ApolloServer. This way, we make sure that the same schema is being used in both places.

    index.ts
    const schema = makeExecutableSchema({ typeDefs, resolvers });
    // ...
    const server = new ApolloServer({
    schema,
    });
  5. Create a WebSocketServer to use as your server.

    index.ts
    // Creating the WebSocket server
    const wsServer = new WebSocketServer({
    // This is the `httpServer` we created in a previous step.
    server: httpServer,
    // Pass a different path here if app.use
    // serves expressMiddleware at a different path
    path: '/subscriptions',
    });
    // Hand in the schema we just created and have the
    // WebSocketServer start listening.
    const serverCleanup = useServer({ schema }, wsServer);
  6. Add plugins to your ApolloServer constructor to shutdown both the HTTP server and the WebSocketServer:

    index.ts
    const server = new ApolloServer({
    schema,
    plugins: [
    // Proper shutdown for the HTTP server.
    ApolloServerPluginDrainHttpServer({ httpServer }),
    // Proper shutdown for the WebSocket server.
    {
    async serverWillStart() {
    return {
    async drainServer() {
    await serverCleanup.dispose();
    },
    };
    },
    },
    ],
    });
  7. Finally, ensure you are listening to your httpServer.

    Most Express applications call app.listen(...), but for your setup change this to httpServer.listen(...) using the same . This way, the server starts listening on the HTTP and WebSocket transports simultaneously.

A completed example of setting up is shown below:

⚠️ Running into an error? If you're using the graphql-ws library, your specified protocol must be consistent across your backend, frontend, and every other tool you use (including Apollo Sandbox). For more information, see Switching from subscriptions-transport-ws.

Resolving a subscription

for Subscription differ from for fields of other types. Specifically, Subscription are objects that define a subscribe function:

index.ts
const resolvers = {
Subscription: {
hello: {
// Example using an async generator
subscribe: async function* () {
for await (const word of ['Hello', 'Bonjour', 'Ciao']) {
yield { hello: word };
}
},
},
postCreated: {
// More on pubsub below
subscribe: () => pubsub.asyncIterator(['POST_CREATED']),
},
},
// ...other resolvers...
};

The subscribe function must return an object of type AsyncIterator, a standard interface for iterating over asynchronous results. In the above postCreated.subscribe , an AsyncIterator is generated by pubsub.asyncIterator (more on this below).

The PubSub class

The PubSub class is not recommended for production environments, because it's an in-memory event system that only supports a single server instance. After you get working in development, we strongly recommend switching it out for a different subclass of the abstract PubSubEngine class. Recommended subclasses are listed in Production PubSub libraries.

You can use the publish-subscribe (pub/sub) model to track events that update active . The graphql-subscriptions library provides the PubSub class as a basic in-memory event bus to help you get started:

To use the graphql-subscriptions package, first install it like so:

npm install graphql-subscriptions

A PubSub instance enables your server code to both publish events to a particular label and listen for events associated with a particular label. We can create a PubSub instance like so:

import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();

Publishing an event

You can publish an event using the publish method of a PubSub instance:

pubsub.publish('POST_CREATED', {
postCreated: {
author: 'Ali Baba',
comment: 'Open sesame',
},
});
  • The first parameter is the name of the event label you're publishing to, as a string.
    • You don't need to register a label name before publishing to it.
  • The second parameter is the payload associated with the event.
    • The payload should include whatever data is necessary for your resolvers to populate the associated Subscription field and its subfields.

When working with , you publish an event whenever a 's return value should be updated. One common cause of such an update is a , but any back-end logic might result in changes that should be published.

As an example, let's say our API supports a createPost :

type Mutation {
createPost(author: String, comment: String): Post
}

A basic for createPost might look like this:

const resolvers = {
Mutation: {
createPost(parent, args, { postController }) {
// Datastore logic lives in postController
return postController.createPost(args);
},
},
// ...other resolvers...
};

Before we persist the new post's details in our datastore, we can publish an event that also includes those details:

const resolvers = {
Mutation: {
createPost(parent, args, { postController }) {
pubsub.publish('POST_CREATED', { postCreated: args });
return postController.createPost(args);
},
},
// ...other resolvers...
};

Next, we can listen for this event in our Subscription 's .

Listening for events

An AsyncIterator object listens for events that are associated with a particular label (or set of labels) and adds them to a queue for processing.

You can create an AsyncIterator by calling the asyncIterator method of PubSub and passing in an array containing the names of the event labels that this AsyncIterator should listen for.

pubsub.asyncIterator(['POST_CREATED']);

Every Subscription 's subscribe function must return an AsyncIterator object.

This brings us back to the code sample at the top of Resolving a subscription:

index.ts
const resolvers = {
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED']),
},
},
// ...other resolvers...
};

With this subscribe function set, uses the payloads of POST_CREATED events to push updated values for the postCreated .

Filtering events

Sometimes, a client should only receive updated data if that data meets certain criteria. To support this, you can call the withFilter helper function in your Subscription 's .

Example

Let's say our server provides a commentAdded , which should notify clients whenever a comment is added to a specified code repository. A client can execute a subscription that looks like this:

subscription ($repoName: String!) {
commentAdded(repoFullName: $repoName) {
id
content
}
}

This presents a potential issue: our server probably publishes a COMMENT_ADDED event whenever a comment is added to any repository. This means that the commentAdded executes for every new comment, regardless of which repository it's added to. As a result, subscribing clients might receive data they don't want (or shouldn't even have access to).

To fix this, we can use the withFilter helper function to control updates on a per-client basis.

Here's an example for commentAdded that uses the withFilter function:

import { withFilter } from 'graphql-subscriptions';
const resolvers = {
Subscription: {
commentAdded: {
subscribe: withFilter(
() => pubsub.asyncIterator('COMMENT_ADDED'),
(payload, variables) => {
// Only push an update if the comment is on
// the correct repository for this operation
return (
payload.commentAdded.repository_name === variables.repoFullName
);
},
),
},
},
// ...other resolvers...
};

The withFilter function takes two parameters:

  • The first parameter is exactly the function you would use for subscribe if you weren't applying a filter.
  • The second parameter is a filter function that returns true if a update should be sent to a particular client, and false otherwise (Promise<boolean> is also allowed). This function takes two parameters of its own:
    • payload is the payload of the event that was published.
    • variables is an object containing all the client provided when initiating their .

Use withFilter to make sure clients get exactly the updates they want (and are allowed to receive).

Basic runnable example

An example server is available on GitHub and CodeSandbox:

Edit server-subscriptions-as4

The server exposes one (numberIncremented) that returns an integer that's incremented on the server every second. Here's an example that you can run against your server:

subscription IncrementingNumber {
numberIncremented
}

If you don't see the result of your you might need to adjust your Sandbox settings to use the graphql-ws protocol.

After you start up the server in CodeSandbox, follow the instructions in the browser to test running the numberIncremented in . You should see the subscription's value update every second.

Operation context

When initializing context for a or , you usually extract HTTP headers and other request metadata from the req object provided to the context function.

For subscriptions, you can extract information from a client's request by adding options to the first passed to the useServer function.

For example, you can provide a context property to add values to your contextValue:

// ...
useServer(
{
// Our GraphQL schema.
schema,
// Adding a context property lets you add data to your GraphQL operation contextValue
context: async (ctx, msg, args) => {
// You can define your own function for setting a dynamic context
// or provide a static value
return getDynamicContext(ctx, msg, args);
},
},
wsServer,
);

Notice that the first parameter passed to the context function above is ctx. The ctx object represents the context of your subscription server, not the GraphQL operation contextValue that's passed to your .

You can access the parameters of a client's subscription request to your WebSocket server via the ctx.connectionParams property.

Below is an example of the common use case of extracting an authentication token from a client subscription request and using it to find the current user:

const getDynamicContext = async (ctx, msg, args) => {
// ctx is the graphql-ws Context where connectionParams live
if (ctx.connectionParams.authentication) {
const currentUser = await findUser(ctx.connectionParams.authentication);
return { currentUser };
}
// Otherwise let our resolvers know we don't have a current user
return { currentUser: null };
};
useServer(
{
schema,
context: async (ctx, msg, args) => {
// Returning an object will add that information to
// contextValue, which all of our resolvers have access to.
return getDynamicContext(ctx, msg, args);
},
},
wsServer,
);

Putting it all together, the useServer.context function returns an object, contextValue, which is available to your .

Note that the context option is called once per subscription request, not once per event emission. This means that in the above example, every time a client sends a subscription , we check their authentication token.

onConnect and onDisconnect

You can configure the server's behavior whenever a client connects (onConnect) or disconnects (onDisconnect).

Defining an onConnect function enables you to reject a particular incoming connection by returning false or by throwing an exception. This can be helpful if you want to check authentication when a client first connects to your server.

You provide these functions as options to the first of useServer, like so:

useServer(
{
schema,
// As before, ctx is the graphql-ws Context where connectionParams live.
onConnect: async (ctx) => {
// Check authentication every time a client connects.
if (tokenIsNotValid(ctx.connectionParams)) {
// You can return false to close the connection or throw an explicit error
throw new Error('Auth token missing!');
}
},
onDisconnect(ctx, code, reason) {
console.log('Disconnected!');
},
},
wsServer,
);

For more examples of using onConnect and onDisconnect, see the graphql-ws recipes documentation.

Example: Authentication with Apollo Client

If you plan to use with , ensure both your client and server subscription protocols are consistent for the library you're using (this example uses the graphql-ws library).

In , the GraphQLWsLink constructor supports adding information to connectionParams (example). Those connectionParams are sent to your server when it connects, allowing you to extract information from the client request by accessing Context.connectionParams.

Let's suppose we create our client like so:

import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
const wsLink = new GraphQLWsLink(
createClient({
url: 'ws://localhost:4000/subscriptions',
connectionParams: {
authentication: user.authToken,
},
}),
);

The connectionParams (which contains the information provided by the client) is passed to your server, enabling you to validate the user's credentials.

From there you can use the useServer.context property to authenticate the user, returning an object that's passed into your as the context during execution.

For our example, we can use the connectionParams.authentication value provided by the client to look up the related user before passing that user along to our :

const findUser = async (authToken) => {
// Find a user by their auth token
};
const getDynamicContext = async (ctx, msg, args) => {
if (ctx.connectionParams.authentication) {
const currentUser = await findUser(ctx.connectionParams.authentication);
return { currentUser };
}
// Let the resolvers know we don't have a current user so they can
// throw the appropriate error
return { currentUser: null };
};
// ...
useServer(
{
// Our GraphQL schema.
schema,
context: async (ctx, msg, args) => {
// This will be run every time the client sends a subscription request
return getDynamicContext(ctx, msg, args);
},
},
wsServer,
);

To sum up, the example above looks up a user based on the authentication token sent with each request before returning the user object to be used by our . If no user exists or the lookup otherwise fails, our resolvers can throw an error and the corresponding subscription is not executed.

Production PubSub libraries

As mentioned above, the PubSub class is not recommended for production environments, because its event-publishing system is in-memory. This means that events published by one instance of your are not received by that are handled by other instances.

Instead, you should use a subclass of the PubSubEngine abstract class that you can back with an external datastore such as Redis or Kafka.

The following are community-created PubSub libraries for popular event-publishing systems:

If none of these libraries fits your use case, you can also create your own PubSubEngine subclass. If you create a new open-source library, click Edit on GitHub to let us know about it!

Switching from subscriptions-transport-ws

If you use with you must ensure both your client and server subscription protocols are consistent for the library you're using.

This article previously demonstrated using the subscriptions-transport-ws library to set up . However, this library is no longer actively maintained. You can still use it with , but we strongly recommend using graphql-ws instead.

For details on how to switch from subscriptions-transport-ws to graphql-ws, follow the steps in the Apollo Server 3 docs.

Updating subscription clients

If you intend to switch from subscriptions-transport-ws to graphql-ws you will need to update the following clients:

Client NameTo use graphql-ws (RECOMMENDED)To use subscriptions-transport-ws

Apollo Studio Explorer

graphql-ws

subscriptions-transport-ws

Apollo Client Web

Use GraphQLWsLink
(Requires v3.5.10 or later)

Use WebSocketLink

Apollo iOS

graphql_transport_ws
(Requires v0.51.0 or later)

graphql_ws

Apollo Kotlin

GraphQLWsProtocol
(Requires v3.0.0 or later)

SubscriptionWsProtocol

Previous
Error handling
Next
Overview
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company