Subscriptions in Apollo Server
Persistent GraphQL read operations
Apollo Server 3 removes built-in support for subscriptions. You can reenable support as described below.
Subscriptions are not currently supported in Apollo Federation.
This article has been updated to use the
graphql-ws
library to add support for subscriptions to Apollo Server. We no longer recommend using the previously documentedsubscriptions-transport-ws
, because this library is not actively maintained. For more information about the differences between the two libraries, see Switching fromsubscriptions-transport-ws
.
Subscriptions are long-lasting GraphQL read operations 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 subscription to push newly received messages to all clients in a particular chat room.
Because subscription updates are usually pushed by the server (instead of polled by the client), they usually use the WebSocket protocol
instead of HTTP.Important: Compared to queries and mutations, subscriptions 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 fields that clients can subscribe to:
1type Subscription {
2 postCreated: Post
3}
The postCreated
field 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
field with a GraphQL string, like this:
1subscription PostFeed {
2 postCreated {
3 author
4 comment
5 }
6}
Each subscription operation can subscribe to only one field of the
Subscription
type.
Enabling subscriptions
Beginning in Apollo Server 3, subscriptions are not supported by the "batteries-included"
apollo-server
package. To enable subscriptions, you must first swap to theapollo-server-express
package (or any other Apollo Server integration package that supports subscriptions).The following steps assume you've already swapped to
apollo-server-express
.
To run both an Express app and a separate WebSocket server for subscriptions, we'll create an http.Server
instance that effectively wraps the two and becomes our new listen
er.
Install
graphql-ws
,ws
,@graphql-tools/schema
, andapollo-server-core
:Bash1npm install graphql-ws ws @graphql-tools/schema apollo-server-core
Add the following imports to the file where you initialize your
ApolloServer
instance (we'll use these in later steps):TypeScriptindex.ts1import { createServer } from 'http'; 2import { 3 ApolloServerPluginDrainHttpServer, 4 ApolloServerPluginLandingPageLocalDefault, 5} from "apollo-server-core"; 6import { makeExecutableSchema } from '@graphql-tools/schema'; 7import { WebSocketServer } from 'ws'; 8import { useServer } from 'graphql-ws/lib/use/ws';
Next, in order to set up both the HTTP and subscription servers, we need to first create an
http.Server
. Do this by passing your Expressapp
to thecreateServer
function, which we imported from thehttp
module:TypeScriptindex.ts1// This `app` is the returned value from `express()`. 2const httpServer = createServer(app);
Create an instance of
GraphQLSchema
(if you haven't already).If you already pass the
schema
option to theApolloServer
constructor (instead oftypeDefs
andresolvers
), you can skip this step.The subscription server (which we'll instantiate next) doesn't take
typeDefs
andresolvers
options. Instead, it takes an executableGraphQLSchema
. We can pass thisschema
object to both the subscription server andApolloServer
. This way, we make sure that the same schema is being used in both places.TypeScriptindex.ts1const schema = makeExecutableSchema({ typeDefs, resolvers }); 2// ... 3const server = new ApolloServer({ 4 schema, 5 csrfPrevention: true, 6 cache: "bounded", 7 plugins: [ 8 ApolloServerPluginLandingPageLocalDefault({ embed: true }), 9 ], 10});
Create a
WebSocketServer
to use as your subscription server.TypeScriptindex.ts1 // Creating the WebSocket server 2 const wsServer = new WebSocketServer({ 3 // This is the `httpServer` we created in a previous step. 4 server: httpServer, 5 // Pass a different path here if your ApolloServer serves at 6 // a different path. 7 path: '/graphql', 8 }); 9 10 // Hand in the schema we just created and have the 11 // WebSocketServer start listening. 12 const serverCleanup = useServer({ schema }, wsServer);
Add plugins to your
ApolloServer
constructor to shutdown both the HTTP server and theWebSocketServer
:TypeScriptindex.ts1 const server = new ApolloServer({ 2 schema, 3 csrfPrevention: true, 4 cache: "bounded", 5 plugins: [ 6 // Proper shutdown for the HTTP server. 7 ApolloServerPluginDrainHttpServer({ httpServer }), 8 9 // Proper shutdown for the WebSocket server. 10 { 11 async serverWillStart() { 12 return { 13 async drainServer() { 14 await serverCleanup.dispose(); 15 }, 16 }; 17 }, 18 }, 19 ApolloServerPluginLandingPageLocalDefault({ embed: true }), 20 ], 21 });
Finally, ensure you are
listen
ing to yourhttpServer
.Most Express applications call
app.listen(...)
, but for your setup change this tohttpServer.listen(...)
using the same arguments. This way, the server starts listening on the HTTP and WebSocket transports simultaneously.
A completed example of setting up subscriptions is shown below:
Click to expand
1import { ApolloServer } from 'apollo-server-express';
2import { createServer } from 'http';
3import express from 'express';
4import { ApolloServerPluginDrainHttpServer } from "apollo-server-core";
5import { makeExecutableSchema } from '@graphql-tools/schema';
6import { WebSocketServer } from 'ws';
7import { useServer } from 'graphql-ws/lib/use/ws';
8import resolvers from './resolvers';
9import typeDefs from './typeDefs';
10
11// Create the schema, which will be used separately by ApolloServer and
12// the WebSocket server.
13const schema = makeExecutableSchema({ typeDefs, resolvers });
14
15// Create an Express app and HTTP server; we will attach both the WebSocket
16// server and the ApolloServer to this HTTP server.
17const app = express();
18const httpServer = createServer(app);
19
20// Create our WebSocket server using the HTTP server we just set up.
21const wsServer = new WebSocketServer({
22 server: httpServer,
23 path: '/graphql',
24});
25// Save the returned server's info so we can shutdown this server later
26const serverCleanup = useServer({ schema }, wsServer);
27
28// Set up ApolloServer.
29const server = new ApolloServer({
30 schema,
31 csrfPrevention: true,
32 cache: "bounded",
33 plugins: [
34 // Proper shutdown for the HTTP server.
35 ApolloServerPluginDrainHttpServer({ httpServer }),
36
37 // Proper shutdown for the WebSocket server.
38 {
39 async serverWillStart() {
40 return {
41 async drainServer() {
42 await serverCleanup.dispose();
43 },
44 };
45 },
46 },
47 ApolloServerPluginLandingPageLocalDefault({ embed: true }),
48 ],
49});
50await server.start();
51server.applyMiddleware({ app });
52
53const PORT = 4000;
54// Now that our HTTP server is fully set up, we can listen to it.
55httpServer.listen(PORT, () => {
56 console.log(
57 `Server is now running on http://localhost:${PORT}${server.graphqlPath}`,
58 );
59});
⚠️ Running into an error? If you're using the
graphql-ws
library, your specified subscription protocol must be consistent across your backend, frontend, and every other tool you use (including Apollo Sandbox. For more information, see Switching fromsubscriptions-transport-ws
.
Resolving a subscription
Resolvers for Subscription
fields differ from resolvers for fields of other types. Specifically, Subscription
field resolvers are objects that define a subscribe
function:
1const resolvers = {
2 Subscription: {
3 hello: {
4 // Example using an async generator
5 subscribe: async function* () {
6 for await (const word of ["Hello", "Bonjour", "Ciao"]) {
7 yield { hello: word };
8 }
9 },
10 },
11 postCreated: {
12 // More on pubsub below
13 subscribe: () => pubsub.asyncIterator(['POST_CREATED']),
14 },
15 },
16 // ...other resolvers...
17};
The subscribe
function must return an object of type AsyncIterator
, a standard interface for iterating over asynchronous results. In the above postCreated.subscribe
field, an AsyncIterator
is generated by pubsub.asyncIterator
(more on this below).
The PubSub
class
The
. Recommended subclasses are listed in ProductionPubSub
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 subscriptions working in development, we strongly recommend switching it out for a different subclass of the abstractPubSubEngine
classPubSub
libraries.
Apollo Server uses a publish-subscribe (pub/sub) model to track events that update active subscriptions. The graphql-subscriptions
library
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:
1npm 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:
1import { PubSub } from 'graphql-subscriptions';
2
3const pubsub = new PubSub();
Publishing an event
You can publish an event using the publish
method of a PubSub
instance:
1pubsub.publish('POST_CREATED', {
2 postCreated: {
3 author: 'Ali Baba',
4 comment: 'Open sesame'
5 }
6});
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 GraphQL subscriptions, you publish
an event whenever a subscription's return value should be updated. One common cause of such an update is a mutation, but any back-end logic might result in changes that should be publish
ed.
As an example, let's say our GraphQL API supports a createPost
mutation:
1type Mutation {
2 createPost(author: String, comment: String): Post
3}
A basic resolver for createPost
might look like this:
1const resolvers = {
2 Mutation: {
3 createPost(parent, args, context) {
4 // Datastore logic lives in postController
5 return postController.createPost(args);
6 },
7 },
8 // ...other resolvers...
9};
Before we persist the new post's details in our datastore, we can publish
an event that also includes those details:
1const resolvers = {
2 Mutation: {
3 createPost(parent, args, context) {
4 pubsub.publish('POST_CREATED', { postCreated: args });
5 return postController.createPost(args);
6 },
7 },
8 // ...other resolvers...
9};
Next, we can listen for this event in our Subscription
field's resolver.
Listening for events
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.
1pubsub.asyncIterator(['POST_CREATED']);
Every Subscription
field resolver's subscribe
function must return an AsyncIterator
object.
This brings us back to the code sample at the top of Resolving a subscription:
1const resolvers = {
2 Subscription: {
3 postCreated: {
4 subscribe: () => pubsub.asyncIterator(['POST_CREATED']),
5 },
6 },
7 // ...other resolvers...
8};
With this subscribe
function set, Apollo Server uses the payloads of POST_CREATED
events to push updated values for the postCreated
field.
Filtering events
Sometimes, a client should only receive updated subscription data if that data meets certain criteria. To support this, you can call the withFilter
helper function in your Subscription
field's resolver.
Example
Let's say our server provides a commentAdded
subscription, which should notify clients whenever a comment is added to a specified code repository. A client can execute a subscription that looks like this:
1subscription($repoName: String!){
2 commentAdded(repoFullName: $repoName) {
3 id
4 content
5 }
6}
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
resolver 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 resolver for commentAdded
that uses the withFilter
function:
1import { withFilter } from 'graphql-subscriptions';
2
3const resolvers = {
4 Subscription: {
5 commentAdded: {
6 subscribe: withFilter(
7 () => pubsub.asyncIterator('COMMENT_ADDED'),
8 (payload, variables) => {
9 // Only push an update if the comment is on
10 // the correct repository for this operation
11 return (payload.commentAdded.repository_name === variables.repoFullName);
12 },
13 ),
14 }
15 },
16 // ...other resolvers...
17};
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 subscription update should be sent to a particular client, andfalse
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 arguments the client provided when initiating their subscription.
Use withFilter
to make sure clients get exactly the subscription updates they want (and are allowed to receive).
Basic runnable example
An example server is available on GitHub
and CodeSandbox:The server exposes one subscription (numberIncremented
) that returns an integer that's incremented on the server every second. Here's an example subscription that you can run against your server:
1subscription IncrementingNumber {
2 numberIncremented
3}
If you don't see the result of your subscription operation 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
subscription in Apollo Sandbox. You should see the subscription's value update every second.
Operation context
When initializing context for a query or mutation, 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 argument passed to theuseServer
function.For example, you can provide a context
property
context
:1// ...
2useServer(
3 {
4 // Our GraphQL schema.
5 schema,
6 // Adding a context property lets you add data to your GraphQL operation context
7 context: (ctx, msg, args) => {
8 // You can define your own function for setting a dynamic context
9 // or provide a static value
10 return getDynamicContext(ctx, msg, args);
11 },
12 },
13 wsServer,
14);
Notice that the first parameter passed to the context
function above is ctx
. The ctx object
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:
1const getDynamicContext = async (ctx, msg, args) => {
2 // ctx is the graphql-ws Context where connectionParams live
3 if (ctx.connectionParams.authentication) {
4 const currentUser = await findUser(ctx.connectionParams.authentication);
5 return { currentUser };
6 }
7 // Otherwise let our resolvers know we don't have a current user
8 return { currentUser: null };
9};
10
11useServer(
12 {
13 schema,
14 // Adding a context property lets you add data to your GraphQL operation context
15 context: (ctx, msg, args) => {
16 // Returning an object will add that information to our
17 // GraphQL context, which all of our resolvers have access to.
18 return getDynamicContext(ctx, msg, args);
19 },
20 },
21 wsServer,
22);
Putting it all together, if the useServer.context
function returns an object, that object is passed to the GraphQL operation context
, which is available to your resolvers.
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
operation, we check their authentication token.
onConnect
and onDisconnect
You can configure the subscription 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 subscription server.
You provide these functions as options to the first argument of useServer
, like so:
1useServer(
2 {
3 schema,
4 // As before, ctx is the graphql-ws Context where connectionParams live.
5 onConnect: async (ctx) => {
6 // Check authentication every time a client connects.
7 if (tokenIsNotValid(ctx.connectionParams)) {
8 // You can return false to close the connection or throw an explicit error
9 throw new Error('Auth token missing!');
10 }
11 },
12 onDisconnect(ctx, code, reason) {
13 console.log('Disconnected!');
14 },
15 },
16 wsServer,
17);
For more information and examples of using onConnect
and onDisconnect
, see the graphql-ws
documentation
Example: Authentication with Apollo Client
If you plan to use subscriptions with Apollo Client, ensure both your client and server subscription protocols are consistent for the subscription library you're using (this example uses the
graphql-ws
library).
In Apollo Client, 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 subscription client like so:
1import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
2import { createClient } from 'graphql-ws';
3
4const wsLink = new GraphQLWsLink(createClient({
5 url: 'ws://localhost:4000/subscriptions',
6 connectionParams: {
7 authentication: user.authToken,
8 },
9}));
The connectionParams
argument (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 resolvers as the context
argument 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 resolvers:
1const findUser = async (authToken) => {
2 // Find a user by their auth token
3};
4
5const getDynamicContext = async (ctx, msg, args) => {
6 if (ctx.connectionParams.authentication) {
7 const currentUser = await findUser(connectionParams.authentication);
8 return { currentUser };
9 }
10 // Let the resolvers know we don't have a current user so they can
11 // throw the appropriate error
12 return { currentUser: null };
13};
14
15// ...
16useServer(
17 {
18 // Our GraphQL schema.
19 schema,
20 context: (ctx, msg, args) => {
21 // This will be run every time the client sends a subscription request
22 return getDynamicContext(ctx, msg, args);
23 },
24 },
25 wsServer,
26);
To sum up, the example above looks up a user based on the authentication token sent with each subscription request before returning the user object to be used by our resolvers. If no user exists or the lookup otherwise fails, our resolvers can throw an error and the corresponding subscription
operation 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 GraphQL server are not received by subscriptions that are handled by other instances.
Instead, you should use a subclass of the PubSubEngine
abstract class
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 subscriptions with Apollo Client you must ensure both your client and server subscription protocols are consistent for the subscription library you're using.
This article previously demonstrated using the subscriptions-transport-ws
library to set up subscriptions. However, this library is no longer actively maintained. You can still use it with Apollo Server, but we strongly recommend using graphql-ws
For more information on using Apollo Server with subscriptions-transport-ws
, you can view the previous version of this article
Updating subscription clients
If you intend to switch from subscriptions-transport-ws
to graphql-ws
you will need to update the following clients:
Client Name | To 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 |
Switching to graphql-ws
The following steps assume you've already swapped to the
apollo-server-express
package.
To switch from subscriptions-transport-ws
to graphql-ws
, you can follow the steps below.
Install
graphql-ws
andws
into your app:Bash1npm install graphql-ws ws
Add the following imports to the file where you initialize your HTTP and subscription servers:
TypeScriptindex.ts1import { WebSocketServer } from 'ws'; 2import { useServer } from 'graphql-ws/lib/use/ws';
Instantiate a
WebSocketServer
to use as your new subscription server. This server will replace theSubscriptionServer
fromsubscriptions-transport-ws
.
1 // Creating the WebSocket subscription server
2 const wsServer = new WebSocketServer({
3 // This is the `httpServer` returned by createServer(app);
4 server: httpServer,
5 // Pass a different path here if your ApolloServer serves at
6 // a different path.
7 path: "/graphql",
8 });
9
10 // Passing in an instance of a GraphQLSchema and
11 // telling the WebSocketServer to start listening
12 const serverCleanup = useServer({ schema }, wsServer);
1 const subscriptionServer = SubscriptionServer.create(
2 {
3 schema,
4 // These are imported from `graphql`.
5 execute,
6 subscribe,
7 },
8 {
9 server: httpServer,
10 path: "/graphql",
11 },
12 );
If your SubscriptionServer
extracts information from a subscription to use in your GraphQL operation context
, you can configure your new WebSocket server to do the same.
For example, if your SubscriptionServer
checks a client's authentication token and adds the current user to the GraphQL operation context:
1 const subscriptionServer = SubscriptionServer.create(
2 {
3 schema,
4 execute,
5 subscribe,
6 // This will check authentication every time a
7 // client first connects to the subscription server.
8 async onConnect(connectionParams) {
9 if (connectionParams.authentication) {
10 const currentUser = await findUser(connectionParams.authentication);
11 return { currentUser };
12 }
13 throw new Error("Missing auth token!");
14 },
15 },
16 {
17 server: httpServer,
18 path: "/graphql",
19 },
20 );
You can replicate this behavior by providing a context
option to the first argument passed to the useServer
function.
In the example below, we check a client's authentication every time they send a subscription
operation. If that user exists, we then add the current user to our GraphQL operation context:
1const getDynamicContext = async (ctx, msg, args) => {
2 // ctx is the `graphql-ws` Context where connectionParams live
3 if (ctx.connectionParams.authentication) {
4 const currentUser = await findUser(connectionParams.authentication);
5 return { currentUser };
6 }
7 return { currentUser: null };
8};
9
10useServer(
11 {
12 schema,
13 // Adding a context property lets you add data to your GraphQL operation context.
14 context: (ctx, msg, args) => {
15 // Returning an object here will add that information to our
16 // GraphQL context, which all of our resolvers have access to.
17 return getDynamicContext(ctx, msg, args);
18 },
19 },
20 wsServer,
21);
See Operation Context for more examples of setting GraphQL operation context
.
Add a plugin to your
ApolloServer
constructor to properly shutdown your newWebSocketServer
:
1 const server = new ApolloServer({
2 schema,
3 plugins: [
4 // Proper shutdown for the WebSocket server.
5 {
6 async serverWillStart() {
7 return {
8 async drainServer() {
9 await serverCleanup.dispose();
10 },
11 };
12 },
13 },
14 ],
15 });
Ensure you are
listen
ing to yourhttpServer
call. You can now remove the code related to yourSubscriptionServer
, start your HTTP server, and test that everything works.
If everything is running smoothly, you can (and should) uninstall subscriptions-transport-ws
. The final step is to update all your subscription clients to ensure they use the same subscription protocol.