GraphQL Subscriptions over AWS API Gateway Web Sockets

graphql subscriptions with aws-JMH-2019-00.jpg

GraphQL Subscriptions and the Apollo Project provide a type-safe and API forward way to interact with server push and event-based requests such as Web Sockets.

Although the Apollo Project provides many implementations for various infrastructures and technologies, it has unfortunately been unable to implement a universal solution for Subscriptions over AWS API Gateway Web Sockets and Lambda. However, GraphQL’s flexibility allows us to implement our own on top of any infrastructure.

The Problem

The current implementations for subscriptions in Apollo Server depend on a stateful server, where the subscriptions and publications occur in the same runtime. This is not what happens in the AWS Lambdas where code execution occur in a new runtime every time a Lambda is invoked. Furthermore, since Lambdas are not long-lived, information must be stored regarding which users have connected over a WebSocket and what subscriptions they have requested from the back-end. Even the particular method in which connection and subscription information is stored is not standardized since you can use a few different AWS products to do so such as DynamoDB or AWS Aurora.

Nevertheless, you can still provide a back-end solution that can be used by Apollo Clients without an Apollo Server.

An Implementation

AWS WebSockets

AWS WebSockets are backed by AWS API Gateway and AWS Lambda. AWS exposes routes to handle that can be wired to trigger lambdas to handle specific events. The `$connect` route forwards connection events to a lambda when a client connects to a WebSocket endpoint. The lambda receives a `connectionId` that identifies a client's unique connection and a callback URL you can use to send messages to a client.

In the `$connect` route, you will define a lambda that will store the connection information and the callback URL in a DynamoDB table that you can query later when a message is ready to respond to a client.

Define Your Table

In your table, you want to express a one-to-many relationship between a client and its many possible subscriptions. To do that, define a `clients` table with the following schema.

GraphQL Subscriptions-JMH-2019-1.png

Your table will define a string hash key called `connectionId` that will always correspond to a client's unique connection ID. You will also define a string range key named `id` that will identify a client record or one of many subscriptions. This pattern is referred to as the adjacency list pattern for which you can read more about here.

Define a `ttl` on all table records so that DynamodDB can clean them up after a connection has expired. When a connection is permitted in the table, you will set the `ttl` attribute to the time of connection plus two hours which is how long a WebSocket connection can last in API Gateway.

Define a Connect Handler

Next, attach a lambda to the `$connect` route.

graphql-subscriptions-JMH-2019-2.png

The lambda will be triggered whenever a client attempts to connect to your WebSocket endpoint. The lambda will then execute the following code.

GraphQL Subscriptions-JMH-2019-3.png

In this handler, persist the client's `connectionId`, the callback URL you will publish messages to, the time of connection, and a `ttl`. Afterward, return a 200 status code to the client. 

GraphQL Subscriptions-JMH-2019-4.png

You will also attach resolvers to your GraphQL schema that will handle a subscription query.

GraphQL Subscriptions-JMH-2019-5.png

The resolver will store the subscription method as an adjacency item under the corresponding client along with the request's ID and the same `ttl` the connection was created with.

Handling the GraphQL Subscription Query

You will now define a subscription handler.

GraphQL Subscriptions-JMH-2019-6.png

The subscription handler is mapped to the `$default` route so that the lambda is triggered on all incoming messages from authorized clients. When this lambda is triggered, you will handle incoming Apollo Client requests.

Any incoming Apollo Client request will have the following format:

GraphQL Subscriptions-JMH-2019-7.png

The `id` field is a unique ID for the request that can be used to identify a corresponding response. The `type` field is used by Apollo Clients to identify the type of operation that is being requested from the back-end. The `payload` is the data for the query.

You can process this request in the lambda like so:

GraphQL Subscriptions-JMH-2019-8.png

In this lambda you do the following:

  • Verify that you have registered a client's connection in your table

  • Check if you have received a `connection_init` request from a JavaScript Apollo Client implementation. If so, return an acknowledgment. To learn more about how the JavaScript Apollo Client connects to a WebSockets endpoint, go here.

  • If it is not an init request, parse the query payload and verify that it is a subscription request. This step can be omitted if the endpoint will handle GraphQL queries other than subscriptions.

  • Validate the query against your schema

    • If the request is invalid, you return an error

    • If the request is valid GraphQL, execute your query so that your resolvers are executed 

  • Finally, return a successful response or an error.

After the lambda completes, you should have the subscription persisted in your table and the client should receive a valid subscription response.

Publishing to a Subscription

Define a lambda that publishes new messages to client subscriptions. This lambda is triggered by SNS events form your `messages` topic.

GraphQL Subscriptions-JMH-2019-9.png

When this lambda is triggered, it will execute the following handler.

GraphQL Subscriptions-JMH-2019-10.png

This lambda will do the following:

  • Query all clients and subscriptions that correspond to the user that the messages are addressed to

  • Validate that the `ttl` for those subscriptions has not expired

  • For each subscription, construct a payload in the format Apollo Client expects

  • Convert the event content into a GraphQL AST value from the Message type and set it as the payload

  • Publishes the message on the client's callback URL

Connecting a Client

On the client-side, connect to your AWS API Gateway Web Sockets endpoint using a WebSocketClient and Apollo Client.

GraphQL Subscriptions-JMH-2019-11.png

By default, JavaScript Apollo Client implementations connect to WebSockets endpoint passing a `Sec-WebSocket-Protocol` set to `graphql`. The connection must respond with the same header for the connection to succeed. At the time of writing, AWS does not allow overriding headers. Instead, connect to the endpoint by overriding the protocol header by setting an empty array to the Subscription client.

Now that the client is connected you can request a subscription to your WebSockets endpoint.

GraphQL Subscriptions-JMH-2019-12.png

When a message is published on the SNS topic, the back-end should publish a new message and the client will show a new message incoming, not the subscription.

GraphQL Subscriptions-JMH-2019-13.png

Conclusion

In this article, we walked through how to leverage the Apollo and GraphQL libraries to implement GraphQL Subscriptions over Web Sockets on AWS.

GraphQL Subscriptions are a powerful method to model event-based communication between web services. They can provide technology like WebSockets type safety and expressiveness that make APIs safer and easier to use. Furthermore, AWS API Gateway and Lambda provide infrastructure that can make any WebSocket implementation highly scalable and flexible.

 
 
Jorge-Hernandez-Martines.jpg

About the Author

Jorge Martinez Hernandez is a Backend Engineer at Yonomi. A wizard with embedded IoT solutions, he works on core functionality, feature development, and device management on the Yonomi ThinCloud team. Jorge likes working with well designed, tested and expressive code. Outside of work, he enjoys learning new languages - he is currently learning french - and spending time with his family in Manor, TX.

 
 

Building a Connected Device for the Smart Home?

Ask us about Yonomi ThinCloud, our scalable, flexible, and cost effective cloud backend for consumer IoT devices.

Learn More