A proposal for GraphQL subscriptions
Sashko Stubailo
Real-time subscriptions have been an exciting topic in the GraphQL community almost from the start. Laney Kuenzel has given several talks about Facebook’s internal implementation of subscriptions, and there have been a few community implementations. Unfortunately, a lot of these have been coupled to specific frameworks or application architectures, so it’s been hard for GraphQL subscriptions to take off as a standard.
We wanted to build something anyone can integrate into their servers and clients, without reinventing the common features any implementation would need. That way, as a community we can start building and sharing integrations for different backends, such as Redis, PostgreSQL triggers, Kafka, and more.
That’s why we are shipping an open-source GraphQL subscriptions implementation designed to be as general as possible.
The core packages
Today, I’d like to introduce you to two packages in particular:
- graphql-subscriptions, a package you can use to manage parsing, pub/sub handling, and execution for GraphQL subscriptions with GraphQL.js, Facebook’s reference JavaScript implementation. This is completely transport and backend independent, and only implements the kinds of things we think could be part of the spec for GraphQL subscriptions some day.
- subscriptions-transport-ws, a server and client implementation of a simple Websocket protocol for GraphQL subscriptions. This is not coupled to any specific GraphQL clients, and can be used from Apollo, Relay, or just plain JavaScript. It implements subscribing, unsubscribing, and receiving new results or errors for subscriptions. You can easily run it alongside your HTTP server, and plug it into your schema via the graphql-subscriptions package above.
We’re getting started on integrating GraphQL subscriptions into our production apps, but we wanted to open source the tools above up front, so that we can all work together on them as a community. If this sounds interesting, try them out, and open an issue or maybe file a PR with an improvement.
graphql-subscriptions
When you build an HTTP-based GraphQL server in JavaScript, you usually use Facebook’s reference GraphQL.js execution engine, which accepts a query string and a schema and returns a result. The graphql-subscriptions package is a small wrapper around GraphQL.js that can execute subscriptions as well.
The main component of graphql-subscriptions is the SubscriptionManager, which takes a schema, a pub-sub handler, and some optional configuration in the form of setup functions, and creates an object which can execute subscriptions in response to pub-sub events.
What is a setup function? Well, it’s simple. While queries and mutations execute in one step, GraphQL subscriptions run in two steps:
- Receive the subscription string, validate it, store it, and set up any state necessary on the server to subscribe to the right channels.
- Receive a pub-sub message on some channel, execute the subscription, and send the result to the client.
The second step is handled by your GraphQL resolvers, as usual. But the first is a new thing, and that’s what the setup functions in the GraphQL subscriptions package do. By default, they just use the subscription field as the channel name, which makes it easy to write simple demos.
The best part about graphql-subscriptions is that it’s totally decoupled from the pub-sub system and transport you are using. For example, you can plug it into a Redis implementation we recently blogged about. What about the transport? Well, before we move on to the transport, let’s run some very simple GraphQL subscriptions using just GraphQL.js and the graphql-subscriptions package:
// npm install graphql graphql-tools graphql-subscriptions
const { PubSub, SubscriptionManager } = require('graphql-subscriptions');
const { makeExecutableSchema } = require('graphql-tools');
// Our "database"
const messages = [];
// Minimal schema
const typeDefs = `
type Query {
messages: [String!]!
}
type Subscription {
newMessage: String!
}
`;
// Minimal resolvers
const resolvers = {
Query: { messages: () => messages },
// This just passes through the pubsub message contents
Subscription: { newMessage: (rootValue) => rootValue },
};
// Use graphql-tools to make a GraphQL.js schema
const schema = makeExecutableSchema({ typeDefs, resolvers });
// Initialize GraphQL subscriptions
const pubsub = new PubSub();
const subscriptionManager = new SubscriptionManager({ schema, pubsub });
// Run a subscription
subscriptionManager.subscribe({
query: `
subscription NewMessageSubscription {
newMessage
}
`,
callback: (err, result) =>
console.log(`New message: ${result.data.newMessage}`),
});
// Create a message
const helloWorldMessage = 'Hello, world!';
// Add it to "database"
messages.push(helloWorldMessage);
// Push it over pubsub, this could be replaced with Redis easily
pubsub.publish('newMessage', helloWorldMessage);
// Prints "New message: Hello, world!" in the console via the subscription!
As you can see, the graphql-subscriptions package is the only thing you need to start running subscriptions against your schema and getting updates. Just like there is often one popular GraphQL execution engine for each server-side language, we think there should be one core subscription implementation, so that people can target it when writing tools and backend integrations. See how this is possible by reading about the Redis integration.
But it’s not quite enough to go off and write a real app; you also need some way to get those results from a client. That’s where the transport comes in.
subscriptions-transport-ws
This package includes a client and server that works with graphql-subscriptions from above. It defines a minimal websocket protocol that only includes support for starting and stopping subscriptions, and receiving data. It doesn’t even support queries and mutations, since we think it’s currently best to do those over the more standard HTTP transport. Here’s a summary of the messages used by the transport at the time of writing:
- Client to server: SUBSCRIPTION_START, SUBSCRIPTION_END
- Server to client: SUBSCRIPTION_SUCCESS, SUBSCRIPTION_FAIL, SUBSCRIPTION_DATA
As you can see, there is nothing extra in there — just the minimal set of operations needed to run subscriptions. It’s designed to run alongside your existing GraphQL setup.
Here’s how easy it is to add GraphQL subscriptions to a basic Node server using this package:
import { createServer } from 'http'; import { SubscriptionServer } from 'subscriptions-transport-ws';const websocketServer = createServer((request, response) => { response.writeHead(404); response.end(); }); websocketServer.listen(WS_PORT, () => console.log( `Websocket Server is now running on http://localhost:${WS_PORT}` )); new SubscriptionServer( { subscriptionManager }, websocketServer );
As you can see we just pass the subscriptionManager we created above into a new SubscriptionServer instance, which attaches itself to a regular Node web server. You can also pass any options you need, such as a per-subscription context.
Now let’s try calling this subscription over the network, instead of before when we had it in-process!
Running a simple subscription in JS
Luckily, the subscriptions-transport-ws package also includes a handy client implementation, which we can use to connect to our server and run some subscriptions:
// To run this code yourself:
// 1. Download and run the demo server: https://github.com/apollostack/frontpage-server
// 2. Use create-react-app to create a build system
// 3. npm install subscriptions-transport-ws
// 4. Replace all of the code in src/ with just this file
import { Client } from 'subscriptions-transport-ws';
const client = new Client('ws://localhost:8090');
client.subscribe({
query: `
subscription UpvotedPostSubscription {
postUpvoted {
title
votes
}
}
`
}, (err, res) => {
if (err) {
console.error(err);
return;
}
console.log(`"${res.postUpvoted.title}" has ${res.postUpvoted.votes} votes now.`);
})
As you can see, this package is very tiny. It only implements the transport, and doesn’t integrate with any fancy libraries, providing only a basic callback-style interface. Together, we can easily set this up to work with observables, Redux, Apollo, Relay, and any other client-side technology we like to use in our apps.
The future
We hope that by the end of the year GraphQL subscriptions are going to be one of the three main operations people expect from their server: query, mutation, and subscription.
To make this happen, there is still some work to do:
- Servers: We’re excited to work with GraphQL server authors to implement support for a common transport, and work on other pub-sub plugins for Kafka, Postgres triggers, and more. Let us know if you’re interested!
- Clients: Apollo has support for subscriptions built in, and we should add integration with other frontend libraries and tools such as Relay and GraphiQL.
- Spec: We’re working hard to figure out what parts of GraphQL subscriptions are common to all implementations, and should be core to the GraphQL specification. Look out for a post about this soon!
The coolest thing about GraphQL is that it’s an open spec and community, not just one implementation. So let’s work together to move it into the future!