Repository: FormidableLabs/urql
Pull request #160 (2018-12-28 16:48:49)

V1 subscriptions
# Adds Subscriptions to V1

This API is mostly inline with v0 with a few changes.

1. `transformExchange` becomes `forwardSubscription(operation, observable): { unsubscribe: () => void}` in ClientOptions.
2. Connect takes an array of subscriptions instead of just one.
3. Changes the `updateSubscription` callback fn to have a `type` parameter as it's first argument, which is the name of the subscription (Allowing the user to differ between multiple subscription responses).

How to use:

```js
import { createClient } from 'urql';
import { SubscriptionClient } from 'subscriptions-transport-ws';

const subscriptionClient = new SubscriptionClient('ws://localhost:3000', {});

const client = createClient({
  url: 'http://localhost:3001/graphql',
  forwardSubscription(operation, observer) {
    return subscriptionClient.request(operation).subscribe({
      next(data) {
        observer.next({ operation, data, error: null });
      },
      error(error) {
        observer.error({ operation, data: null, error });
      },
    });
  }
});
```

## Internally

Internally a few notable things are happening
1. Exchanges are given `clientOptions` as a new key in the argument.
1. `ClientInstance` gets an `executeSubscription` method
1. `ClientInstance` gets an `executeUnsubscribeSubscription` method
1. `ClientInstance` configuration has a new callback method specifically for subscriptions: `onSubscriptionUpdate`
1. a new exchange is created for subscriptions
1. `operationName` internally are switched from a string to an `enum`

## other stuff

1. Backend is updated to support subscriptions

blainekasten: Just pushed a big change to handle subscription prop changes and unmounting to properly dispose of subscription connections.
andyrichardson: I like the `forwardSubscription` idea, To simplify things and remove the need for mutation, can we do something like this?

```
createClient({
  url: 'http://localhost:3001/graphql',
  onSubscription: (payload: string) => subscriptionClient.request(payload),
});
```

This way the user can return an observable/subject for the graphql request.

I would have some questions about this approach:
 * Your example looks to use a new connection for each subscription query. Is that the case, and if so, is that the common practice for most graphql subscription/websocket clients? 
 * Are we handling connection states such as an the closing of a connection?
blainekasten: > I like the forwardSubscription idea, To simplify things and remove the need for mutation, can we do something like this?
>
> ```js
> createClient({
>   url: 'http://localhost:3001/graphql',
>   onSubscription: (payload: string) => subscriptionClient.request(payload),
> });
> ```
> This way the user can return an observable/subject for the graphql request.

I'm not sure that simplifies anything actually. In reality, I need the response to include the unsubscribe method so I can remove graphql subscription connections when a subscription unmounts or changes.


> I would have some questions about this approach:
>
> Your example looks to use a new connection for each subscription query. Is that the case, and if so, is that the common practice for most graphql subscription/websocket clients?
> Are we handling connection states such as an the closing of a connection?

Actually it's doing the opposite. When you init the `subscriptionClient` that is what creates the websocket connection. Internally, every time you run `executeSubscription` it sends a frame through the ws connection for the backend to know about:

![image](https://user-images.githubusercontent.com/2916023/50661075-95283880-0f67-11e9-9114-18dd0f4a1588.png)


Then, when you run `executeUnsubscribeSubscription` it sends new frames to tell the backend they have stopped:

![image](https://user-images.githubusercontent.com/2916023/50661099-aa04cc00-0f67-11e9-9a58-6b0be82ce230.png)

Patch:
diff --git a/MIGRATION.md b/MIGRATION.md
index ff21f582..a8dd5ff4 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -3,6 +3,7 @@
 ## Additions
 
 - `Client` / `createClient` config object has additional optional value `exchanges`.
+- `Client` adds a config option for handling subscriptions: `forwardSubscription` with a type of `(operation, observer) => {unsubscribe: () => void}`
 - `Connect` child function argument `mutations` is now typed.
 
 ## Changes
@@ -13,6 +14,8 @@
 - `createClient` returns an object with a function for creating a client instance - see documentation for further information.
 - `Connect` component prop `query` now only supports a single query element (multiple GraphQL query strings can be declared in a single Query object).
 - `Connect` component prop `mutation` is now named `mutations`.
+- `Connect` component prop `subscription` is now named `subscriptions` and is an array of subscriptions.
+- `Connect` component prop `updateSubscription` signature slightly changed to the following: `(type, state, data) => newState`.
 - `Connect` child function argument now groups mutations into a single `mutations` property.
 - `Connect` child function argument property `refetch` now takes a single boolean value for refreshing cache.
 - `Exchanges` have changed substantially, please see documentation for more information (default exchanges should work as expected).
diff --git a/docs/README.md b/docs/README.md
index 547ba6b6..ede56dc2 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -20,4 +20,4 @@
 
 - [ChildArgs](types/child-args.md)
 - [CombinedError](types/combined-error.md)
-- [Mutation/Query](types/mutation-query.md)
+- [Mutation/Query/Subscription](types/mutation-query-subscription.md)
diff --git a/docs/about/client.md b/docs/about/client.md
index 81293bd8..73e9fdc3 100644
--- a/docs/about/client.md
+++ b/docs/about/client.md
@@ -11,6 +11,10 @@ import { createClient, createQuery } from 'urql';
 
 const client = createClient({
   url: 'https://my-host/graphql',
+  forwardSubscription(operation, observer) => {
+    observer.next({data: {}, error: null, operation}),
+    return { unsubscribe };
+  },
 });
 ```
 
@@ -28,6 +32,10 @@ const todoClient = client.createInstance({
     console.log('The following values changed: ', changed);
     myState = { ...myState, ...changed };
   },
+  onSubscriptionUpdate: update => {
+    console.log('new data pushed from the server!', update);
+    myState = { ...myState, ...update };
+  },
 });
 ```
 
@@ -57,6 +65,26 @@ const mutation = createMutation(AddTodo, args);
 todoClient.executeMutation(mutation);
 ```
 
+### Executing a subscription
+
+Executing a subscription works in a similar way to executing a query.
+
+> Notice! In order to use subscriptions, your client needs to be configured with a `forwardSubscription` option.
+
+```jsx
+const subscription = createSubscription(TodoAdded, args);
+
+todoClient.executeSubscription(subscription);
+```
+
+**Unsubscribe**
+
+To unsubscribe from a subscription with your backend, pass the same subscription to the unsubscribe function.
+
+```jsx
+todoClient.executeUnsubscribeSubscription(subscription);
+```
+
 ### Terminating the client
 
 Spoiler alert! The magic behind queries being automatically fetched is by-part thanks to the use of streaming. This comes with a catch however as our client instance is now latched onto a stream.
diff --git a/docs/about/subscriptions.md b/docs/about/subscriptions.md
new file mode 100644
index 00000000..5be9c826
--- /dev/null
+++ b/docs/about/subscriptions.md
@@ -0,0 +1,80 @@
+## Urql Subscriptions
+
+Urql holds a transport-agnostic opinion and API on subscriptions because the GraphQL spec does not define subscriptions yet. Urql will handle the internal
+state and updates, while delegating the actual request operation to a third part (e.g., `subscriptions-transport-ws`).
+
+There are a few steps to make subscriptions work:
+
+1. Create a client with a `forwardSubscription` option.
+1. pass subscriptions into a Connect component or HOC configuration.
+1. pass an `updateSubscription` method to handle the merging of state.
+
+In more detail:
+
+### Creating a client
+
+To get started, as usual, you will want to create a client but make sure to pass in the `forwardSubscription` handler. It has a method signature of:
+
+```jsx
+type forwardSubscription = (
+  operation: Operation,
+  observer: Observer
+) => { unsubscribe: () => void };
+```
+
+This works out especially well with Apollo's `subscriptions-transport-ws` as it returns the same unsubscribe shape. Here's an example of it:
+
+```jsx
+import { createClient, createQuery } from 'urql';
+import { SubscriptionClient } from 'subscriptions-transport-ws';
+const subscriptionClient = new SubscriptionClient(
+  'ws://localhost:3001/graphql',
+  {}
+);
+
+const client = createClient({
+  url: 'https://my-host/graphql',
+  forwardSubscription(operation, observer) => subscribe.request(operation).subscribe({
+    next: (data) => observer.next({data, error: null, operation}),
+    error: (data) => observer.error({data: null, error, operation}),
+  })
+});
+```
+
+### Component/HOC definitions
+
+Subscriptions are an array for multiple subscriptions a component can listen to. In an example todo app, a subscription for when a todo is added will look like this:
+
+```jsx
+import { createSubscription, Connect, ConnectHOC } from 'urql';
+
+const subscriptions = [createSubscription(TodoAdded)];
+
+// <Connect {...subscriptions} />
+// or
+// ConnectHOC({subscriptions})
+```
+
+### updateSubscription handler
+
+When a subscription receives a new event from the server, it is up to the user to manage the merging of old state, with the new change. This handler is attached to the component/HOC and looks like this:
+
+```jsx
+<Connect
+  subscriptions={[
+    createSubscription(TodoAdded),
+    createSubscription(TodoRemoved),
+  ]}
+  updateSubscription={(type, state, data) => {
+    if (type === 'todoAdded') {
+      state.push(data);
+      return state;
+    }
+
+    if (type === 'todoRemoved') {
+      state.splice(state.indexOf(data), 1);
+      return state;
+    }
+  }}
+/>
+```
diff --git a/docs/components/connect-hoc.md b/docs/components/connect-hoc.md
index 7fba62fe..b64896ad 100644
--- a/docs/components/connect-hoc.md
+++ b/docs/components/connect-hoc.md
@@ -4,15 +4,17 @@ A HOC alternative implementation to the [Connect](Connect) component.
 
 ### Props
 
-| Name      | Type                                                         | Default   | Description                                                  |
-| --------- | ------------------------------------------------------------ | --------- | ------------------------------------------------------------ |
-| query     | [Query](../types/mutation-query.md)?                         | undefined | The query you want connected to your component.              |
-| mutations | Object { [string]: [Mutation](../types/mutation-query.md) }? | undefined | The mutation/mutations you want connected to your component. |
+| Name               | Type                                                                      | Default   | Description                                                        |
+| ------------------ | ------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------ |
+| query              | [Query](../types/mutation-query-subscription.md)?                         | undefined | The query you want connected to your component.                    |
+| mutations          | Object { [string]: [Mutation](../types/mutation-query-subscription.md) }? | undefined | The mutation/mutations you want connected to your component.       |
+| subscription       | Array<[Subscription](../types/mutation-query-subscription.md)>?           | undefined | The subscriptions you want connected to your component.            |
+| updateSubscription | (type, state, data) => newData?                                           | undefined | An updator function to merge the new data with the existing state. |
 
 ### Example
 
 ```jsx
-import { ConnectHOC, createMutation, createQuery } from 'urql';
+import { ConnectHOC, createMutation, createQuery, createSubscription } from 'urql';
 import { TodoList } from './components';
 
 // ...
@@ -21,6 +23,13 @@ const connectArgs = {
   mutations: {
     addTodos: createMutation(AddTodos),
   },
+  subscriptions: [createSubscription(TodoAdded)]
+  updateSubscription(type, state, data) => {
+    if (type === 'todoAdded') {
+      state.push(data);
+      return state;
+    }
+  },
 };
 
 // ...
diff --git a/docs/components/connect.md b/docs/components/connect.md
index 9411fb3c..7ecf6126 100644
--- a/docs/components/connect.md
+++ b/docs/components/connect.md
@@ -4,16 +4,18 @@ Connect is a ReactJS component that is used to execute queries and mutations and
 
 ### Props
 
-| Name      | Type                                                         | Default   | Description                                                        |
-| --------- | ------------------------------------------------------------ | --------- | ------------------------------------------------------------------ |
-| query     | [Query](../types/mutation-query.md)?                         | undefined | The query you want connected to your component                     |
-| mutations | Object { [string]: [Mutation](../types/mutation-query.md) }? | undefined | The mutation/mutations you want connected to your component        |
-| children  | ([ChildArgs\<T\>](../types/child-args.md)) => ReactNode      | undefined | A child function to accept the connected state and render elements |
+| Name               | Type                                                                      | Default   | Description                                                        |
+| ------------------ | ------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------ |
+| query              | [Query](../types/mutation-query-subscription.md)?                         | undefined | The query you want connected to your component                     |
+| mutations          | Object { [string]: [Mutation](../types/mutation-query-subscription.md) }? | undefined | The mutation/mutations you want connected to your component        |
+| subscription       | Array<[Subscription](../types/mutation-query-subscription.md)>?           | undefined | The subscriptions you want connected to your component.            |
+| updateSubscription | (type, state, data) => newData?                                           | undefined | An updator function to merge the new data with the existing state. |
+| children           | ([ChildArgs\<T\>](../types/child-args.md)) => ReactNode                   | undefined | A child function to accept the connected state and render elements |
 
 ### Example
 
 ```jsx
-import { Connect, createQuery, createMutation } from 'urql';
+import { Connect, createQuery, createMutation, createSubscription } from 'urql';
 
 // ...
 <Connect
@@ -21,6 +23,13 @@ import { Connect, createQuery, createMutation } from 'urql';
   mutations={{
     addTodo: createMutation(AddTodo)
   }}
+  subscriptions={[createSubscription(TodoAdded)]}
+  updateSubscription={(type, state, data) => {
+    if (type === 'todoAdded') {
+      state.push(data);
+      return state;
+    }
+  }}
   children={({ fetching, data }) => {
     return fetching ? <Loading/> : <List data={data.todos}>
   }}
diff --git a/docs/types/mutation-query.md b/docs/types/mutation-query.md
deleted file mode 100644
index c4b6772c..00000000
--- a/docs/types/mutation-query.md
+++ /dev/null
@@ -1,10 +0,0 @@
-## Mutation / Query
-
-A Graphql mutation / query object.
-
-### Properties
-
-| Name      | Type    | Description              |
-| --------- | ------- | ------------------------ |
-| query     | string  | Graphql query string.    |
-| variables | object? | Graphql query variables. |
diff --git a/docs/utils/createMutation.md b/docs/utils/createMutation.md
index 7c5202ad..b0cc4ef1 100644
--- a/docs/utils/createMutation.md
+++ b/docs/utils/createMutation.md
@@ -1,6 +1,6 @@
 ### Description
 
-Create a [Mutation](../types/mutation-query.md) from a mutation query.
+Create a [Mutation](../types/mutation-query-subscription.md) from a mutation query.
 
 ### Arguments
 
diff --git a/docs/utils/createQuery.md b/docs/utils/createQuery.md
index ffef2d6e..748d79ec 100644
--- a/docs/utils/createQuery.md
+++ b/docs/utils/createQuery.md
@@ -1,6 +1,6 @@
 ### Description
 
-Create a [Query](../types/mutation-query.md) from a GraphQL query.
+Create a [Query](../types/mutation-query-subscription.md) from a GraphQL query.
 
 ### Arguments
 
diff --git a/docs/utils/createSubscription.md b/docs/utils/createSubscription.md
new file mode 100644
index 00000000..52066db3
--- /dev/null
+++ b/docs/utils/createSubscription.md
@@ -0,0 +1,27 @@
+### Description
+
+Create a [Subscription](../types/mutation-query-subscription.md) from a GraphQL query.
+
+### Arguments
+
+| Name      | Type    | Description                              |
+| --------- | ------- | ---------------------------------------- |
+| query     | string  | A GraphQL subscription string.           |
+| variables | object? | A collection of GraphQL query variables. |
+
+### Example
+
+```jsx
+import { createSubscription } from 'urql';
+
+const subscriptionString = `
+subscription {
+  todoAdded {
+    id
+    text
+  }
+}
+`;
+
+const subscription = createSubscription(subscriptionString);
+```
diff --git a/example/src/app/home.tsx b/example/src/app/home.tsx
index 6198758d..9875a8e8 100644
--- a/example/src/app/home.tsx
+++ b/example/src/app/home.tsx
@@ -1,7 +1,12 @@
 /* tslint:disable */
 
 import * as React from 'react';
-import { Connect, createQuery, createMutation } from '../../../src/';
+import {
+  Connect,
+  createQuery,
+  createMutation,
+  createSubscription,
+} from '../../../src/';
 import TodoList from './todo-list';
 import TodoForm from './todo-form';
 
@@ -17,37 +22,111 @@ export interface ITodoMutations {
   removeTodo: (input: { id: string }) => void;
 }
 
-const Home: React.SFC<{}> = () => (
-  <Connect
-    query={createQuery(TodoQuery)}
-    mutations={{
-      addTodo: createMutation(AddTodo),
-      removeTodo: createMutation(RemoveTodo),
-    }}
-    children={({ data, error, mutations, fetching, refetch }) => {
-      const content = fetching ? (
-        <Loading />
-      ) : error ? (
-        <Error />
-      ) : (
-        <TodoList todos={data.todos} removeTodo={mutations.removeTodo} />
-      );
-
-      return (
-        <div>
-          {content}
-          <TodoForm addTodo={mutations.addTodo} />
-          <button type="button" onClick={() => refetch()}>
-            Refetch
-          </button>
-          <button type="button" onClick={() => refetch(true)}>
-            Refetch (Skip Cache)
-          </button>
-        </div>
-      );
-    }}
-  />
-);
+const handleSubscription = (type, data, todo) => {
+  switch (type) {
+    case 'todoAdded':
+      if (data.todos.find(t => t.id === todo.id) === undefined) {
+        data.todos.push(todo);
+      }
+      break;
+    case 'todoRemoved':
+      data.todos.splice(data.todos.findIndex(t => t.id === todo.id), 1);
+      break;
+  }
+
+  return data;
+};
+
+class Home extends React.Component<
+  {},
+  { attachAddSubscriptions: boolean; attachRemoveSubscriptions: boolean }
+> {
+  state = {
+    attachAddSubscriptions: false,
+    attachRemoveSubscriptions: false,
+  };
+
+  defaultSubscriptions = [
+    createSubscription(TodoAdded),
+    createSubscription(TodoRemoved),
+  ];
+
+  render() {
+    const subscriptions = [];
+
+    this.state.attachAddSubscriptions &&
+      subscriptions.push(createSubscription(TodoAdded));
+    this.state.attachRemoveSubscriptions &&
+      subscriptions.push(createSubscription(TodoRemoved));
+
+    return (
+      <Connect
+        query={createQuery(TodoQuery)}
+        mutations={{
+          addTodo: createMutation(AddTodo),
+          removeTodo: createMutation(RemoveTodo),
+        }}
+        subscriptions={subscriptions}
+        updateSubscription={handleSubscription}
+        children={({ data, error, mutations, fetching, refetch }) => {
+          const content = fetching ? (
+            <Loading />
+          ) : error ? (
+            <Error />
+          ) : (
+            <TodoList todos={data.todos} removeTodo={mutations.removeTodo} />
+          );
+
+          return (
+            <div>
+              {content}
+              <TodoForm addTodo={mutations.addTodo} />
+              <button type="button" onClick={() => refetch()}>
+                Refetch
+              </button>
+              <button type="button" onClick={() => refetch(true)}>
+                Refetch (Skip Cache)
+              </button>
+              <br />
+              <br />
+              <div>
+                <div>TodoAdded Subscription</div>
+                <button
+                  type="button"
+                  onClick={() =>
+                    this.setState(s => ({
+                      attachAddSubscriptions: !s.attachAddSubscriptions,
+                    }))
+                  }
+                >
+                  {this.state.attachAddSubscriptions
+                    ? 'Deactivate'
+                    : 'Activate'}
+                </button>
+              </div>
+
+              <div>
+                <div>TodoRemoved Subscription</div>
+                <button
+                  type="button"
+                  onClick={() =>
+                    this.setState(s => ({
+                      attachRemoveSubscriptions: !s.attachRemoveSubscriptions,
+                    }))
+                  }
+                >
+                  {this.state.attachRemoveSubscriptions
+                    ? 'Deactivate'
+                    : 'Activate'}
+                </button>
+              </div>
+            </div>
+          );
+        }}
+      />
+    );
+  }
+}
 
 const Loading = () => <p>Loading...</p>;
 
@@ -82,4 +161,21 @@ query {
 }
 `;
 
+const TodoAdded = `
+subscription todoAdded {
+  todoAdded {
+    id
+    text
+  }
+}
+`;
+
+const TodoRemoved = `
+subscription todoRemoved {
+  todoRemoved {
+    id
+  }
+}
+`;
+
 export default Home;
diff --git a/example/src/app/index.tsx b/example/src/app/index.tsx
index 54c47b37..c4cad9a0 100644
--- a/example/src/app/index.tsx
+++ b/example/src/app/index.tsx
@@ -1,11 +1,26 @@
 import * as React from 'react';
 import * as ReactDOM from 'react-dom';
-
 import { Provider, createClient } from '../../../src/index';
 import Home from './home';
 
+import { SubscriptionClient } from 'subscriptions-transport-ws';
+const subscriptionClient = new SubscriptionClient(
+  'ws://localhost:3001/graphql',
+  {}
+);
+
 const client = createClient({
   url: 'http://localhost:3001/graphql',
+  forwardSubscription(operation, observer) {
+    return subscriptionClient.request(operation).subscribe({
+      next(data) {
+        observer.next({ operation, data, error: null });
+      },
+      error(error) {
+        observer.error({ operation, data: null, error });
+      },
+    });
+  },
 });
 
 export const App: React.SFC<{}> = () => (
diff --git a/example/src/server/index.js b/example/src/server/index.js
index 29451c8c..f2432572 100644
--- a/example/src/server/index.js
+++ b/example/src/server/index.js
@@ -1,27 +1,41 @@
-const graphqlHttp = require('express-graphql');
+const { ApolloServer, graphiqlExpress } = require('apollo-server-express');
+const { createServer } = require('http');
+const { SubscriptionServer } = require('subscriptions-transport-ws');
+const expressPlayground = require('graphql-playground-middleware-express')
+  .default;
 const express = require('express');
 const app = express();
 const cors = require('cors');
 
-app.use(cors());
-
-const { schema, context } = require('./schema');
+const { typeDefs, resolvers } = require('./schema');
 
 const PORT = 3001;
 
-const initializedGraphQLMiddleware = graphqlHttp({
-  // GraphQL’s data schema
-  schema: schema,
-  // Pretty Print the JSON response
-  pretty: true,
-  // Enable GraphiQL dev tool
-  graphiql: true,
-  // A function that returns extra data available to every resolver
-  context: context,
+const server = new ApolloServer({
+  typeDefs,
+  resolvers,
 });
 
-app.use(initializedGraphQLMiddleware);
+server.applyMiddleware({ app });
+
+const webServer = createServer(app);
+server.installSubscriptionHandlers(webServer);
+
+const graphqlEndpoint = `http://localhost:${PORT}${server.graphqlPath}`;
+const subscriptionEndpoint = `ws://localhost:${PORT}${
+  server.subscriptionsPath
+}`;
+
+app.use(cors());
+app.get(
+  '/',
+  expressPlayground({
+    endpoint: graphqlEndpoint,
+    subscriptionEndpoint: subscriptionEndpoint,
+  })
+);
 
-app.listen(PORT, () => {
-  console.log(`Server running at http://localhost:${PORT}`);
+webServer.listen(PORT, () => {
+  console.log(`🚀 Server ready at ${graphqlEndpoint}`);
+  console.log(`🚀 Subscriptions ready at ${subscriptionEndpoint}`);
 });
diff --git a/example/src/server/schema.js b/example/src/server/schema.js
index 39eae4b4..fe5f2d93 100644
--- a/example/src/server/schema.js
+++ b/example/src/server/schema.js
@@ -1,6 +1,8 @@
 const fetch = require('isomorphic-fetch');
-const { makeExecutableSchema } = require('graphql-tools');
 const uuid = require('uuid/v4');
+const { PubSub } = require('graphql-subscriptions');
+
+const pubsub = new PubSub();
 
 const store = {
   todos: [
@@ -34,6 +36,11 @@ const typeDefs = `
     removeTodo(id: ID!): Todo
     editTodo(id: ID!, text: String!): Todo
   }
+  type Subscription {
+    todoAdded: Todo
+    todoRemoved: Todo
+    todoUpdated: Todo
+  }
   type Todo {
     id: ID,
     text: String,
@@ -60,18 +67,24 @@ const resolvers = {
     addTodo: (root, args, context) => {
       const id = uuid();
       const { text } = args;
-      store.todos.push({ id, text });
-      return { id, text };
+      const todo = { id, text };
+
+      store.todos.push(todo);
+      pubsub.publish('todoAdded', { todoAdded: { id, text: { text } } });
+
+      return todo;
     },
     removeTodo: (root, args, context) => {
       const { id } = args;
       let todo = store.todos.find(todo => todo.id === id);
       store.todos.splice(store.todos.indexOf(todo), 1);
+      pubsub.publish('todoRemoved', { todoRemoved: todo });
       return { id };
     },
     editTodo: (root, args, context) => {
       const { id, text } = args;
       let todo = store.todos.some(todo => todo.id === id);
+      pubsub.publish('todoUpdated', { todoUpdated: todo });
       todo.text = text;
       return {
         text,
@@ -79,13 +92,22 @@ const resolvers = {
       };
     },
   },
+  Subscription: {
+    todoAdded: {
+      subscribe: () => pubsub.asyncIterator('todoAdded'),
+    },
+    todoRemoved: {
+      subscribe: () => pubsub.asyncIterator('todoRemoved'),
+    },
+    todoUpdated: {
+      subscribe: () => pubsub.asyncIterator('todoUpdated'),
+    },
+  },
 };
 
 module.exports = {
-  schema: makeExecutableSchema({
-    typeDefs,
-    resolvers,
-  }),
+  typeDefs,
+  resolvers,
   context: (headers, secrets) => {
     return {
       headers,
diff --git a/package.json b/package.json
index 1531a5ee..94fc6202 100644
--- a/package.json
+++ b/package.json
@@ -74,6 +74,7 @@
     "@types/react": "^16.0.34",
     "@types/react-dom": "^16.0.3",
     "@types/uuid": "^3.4.3",
+    "apollo-server-express": "^2.3.1",
     "awesome-typescript-loader": "^5.2.1",
     "builder": "^3.2.3",
     "bundlesize": "^0.17.0",
@@ -83,8 +84,8 @@
     "enzyme-adapter-react-16": "^1.7.0",
     "enzyme-to-json": "^3.3.5",
     "express": "^4.16.2",
-    "express-graphql": "^0.7.1",
-    "graphql-tools": "^4.0.3",
+    "graphql-playground-middleware-express": "^1.7.8",
+    "graphql-subscriptions": "^1.0.0",
     "husky": "^1.2.0",
     "isomorphic-fetch": "^2.2.1",
     "jest": "^23.6.0",
@@ -98,6 +99,7 @@
     "react-test-renderer": "^16.2.0",
     "regenerator-runtime": "^0.11.1",
     "rimraf": "^2.6.2",
+    "subscriptions-transport-ws": "^0.9.15",
     "ts-jest": "^23.10.5",
     "tslint": "^5.11.0",
     "tslint-config-prettier": "^1.16.0",
diff --git a/src/components/client.tsx b/src/components/client.tsx
index 5c459506..2cdcf850 100755
--- a/src/components/client.tsx
+++ b/src/components/client.tsx
@@ -7,6 +7,8 @@ import {
   Mutation,
   Query,
   StreamUpdate,
+  Subscription,
+  SubscriptionStreamUpdate,
 } from '../types';
 
 export interface ClientState<MutationDeclarations> {
@@ -22,6 +24,12 @@ export interface ClientProps<MutationDeclarations> {
   children: (obj: ChildArgs<MutationDeclarations>) => ReactNode;
   query?: Query;
   mutations?: { [type in keyof MutationDeclarations]: Mutation };
+  subscriptions?: Subscription[];
+  updateSubscription?: (
+    type: string,
+    prev: object | null,
+    next: object | null
+  ) => object | null;
 }
 
 /** Component responsible for implementing the [Client]{@link Client} utility in React. */
@@ -35,18 +43,29 @@ export class UrqlClient<MutationDeclarations> extends Component<
     this.state = {
       client: props.client.createInstance({
         onChange: result => this.onStreamUpdate(result),
+        onSubscriptionUpdate: result => this.onSubscriptionStreamUpdate(result),
       }),
       data: undefined,
       error: undefined,
       fetching: true,
       mutations: this.getMutatorFunctions(),
     };
+
+    if (process.env.NODE_ENV !== 'production') {
+      if (props.subscriptions && !props.updateSubscription) {
+        throw new Error(
+          'You instantiated an Urql Client component with a subscription but forgot an updateSubscription prop. updateSubscription callbacks are required to work with subscriptions.'
+        );
+      }
+    }
   }
 
   public componentDidMount() {
     if (this.props.query !== undefined) {
       this.state.client.executeQuery(this.props.query);
     }
+
+    this.activateSubscriptions();
   }
 
   public componentDidUpdate(prevProps: ClientProps<MutationDeclarations>) {
@@ -63,9 +82,30 @@ export class UrqlClient<MutationDeclarations> extends Component<
     ) {
       this.state.client.executeQuery(this.props.query);
     }
+
+    if (
+      JSON.stringify(prevProps.subscriptions) !==
+      JSON.stringify(this.props.subscriptions)
+    ) {
+      this.deactiveSubscriptions(prevProps.subscriptions);
+      this.activateSubscriptions();
+    }
   }
 
+  deactiveSubscriptions = (subscriptions: Subscription[] = []) => {
+    subscriptions.forEach(sub =>
+      this.state.client.executeUnsubscribeSubscription(sub)
+    );
+  };
+
+  activateSubscriptions = () => {
+    (this.props.subscriptions || []).forEach(sub =>
+      this.state.client.executeSubscription(sub)
+    );
+  };
+
   public componentWillUnmount() {
+    this.deactiveSubscriptions(this.props.subscriptions);
     this.state.client.unsubscribe();
   }
 
@@ -114,4 +154,21 @@ export class UrqlClient<MutationDeclarations> extends Component<
       data: updated.data,
     });
   }
+
+  private onSubscriptionStreamUpdate(updated: SubscriptionStreamUpdate) {
+    const [type, data] = Object.entries(updated.data)[0];
+
+    if (process.env.NODE_ENV !== 'production') {
+      if (updated.error) {
+        throw updated.error;
+      }
+    }
+
+    this.setState({
+      error: updated.error,
+      data: this.props.updateSubscription
+        ? this.props.updateSubscription(type, this.state.data, data)
+        : this.state.data,
+    });
+  }
 }
diff --git a/src/components/connect-hoc.tsx b/src/components/connect-hoc.tsx
index 8ec1c662..c4b3ca60 100755
--- a/src/components/connect-hoc.tsx
+++ b/src/components/connect-hoc.tsx
@@ -8,6 +8,10 @@ export interface ConnectHOCProps<T> {
   query?: ConnectProps<T>['query'];
   /** A collection of GrahpQL mutation queries. */
   mutations?: ConnectProps<T>['mutations'];
+  /** A collection of GrahpQL subscription queries. */
+  subscriptions?: ConnectProps<T>['subscriptions'];
+  /** A collection of GrahpQL subscription queries. */
+  updateSubscription?: ConnectProps<T>['updateSubscription'];
 }
 
 /** A HOC alternative implementation to the [Connect]{@link Connect} component. */
diff --git a/src/components/connect.tsx b/src/components/connect.tsx
index 66638470..0801af3e 100755
--- a/src/components/connect.tsx
+++ b/src/components/connect.tsx
@@ -1,5 +1,5 @@
 import React, { ReactNode } from 'react';
-import { ChildArgs, Client, Mutation, Query } from '../types';
+import { ChildArgs, Client, Mutation, Query, Subscription } from '../types';
 import { UrqlClient } from './client';
 import { ContextConsumer } from './context';
 
@@ -11,6 +11,14 @@ export interface ConnectProps<T> {
   query?: Query;
   /** A collection of GrahpQL mutation queries */
   mutations?: { [type in keyof T]: Mutation };
+  /** An array of GrahpQL subscription queries */
+  subscriptions?: Subscription[];
+  /** An updator function for merging subscription responses */
+  updateSubscription?: (
+    type: string,
+    prev: object | null,
+    next: object | null
+  ) => object | null;
 }
 
 /** Component for connecting to the urql client for executing queries, mutations and returning the result to child components. */
@@ -23,6 +31,8 @@ export const Connect = function<T>(props: ConnectProps<T>) {
           children={props.children}
           query={props.query}
           mutations={props.mutations}
+          subscriptions={props.subscriptions}
+          updateSubscription={props.updateSubscription}
         />
       )}
     </ContextConsumer>
diff --git a/src/exchanges/cache.ts b/src/exchanges/cache.ts
index f085d74e..f8dc1c6b 100755
--- a/src/exchanges/cache.ts
+++ b/src/exchanges/cache.ts
@@ -1,7 +1,7 @@
 import { merge, Subject } from 'rxjs';
 import { map, partition, share, tap } from 'rxjs/operators';
 import { formatTypeNames, gankTypeNamesFromResponse } from '../lib/typenames';
-import { Exchange, ExchangeResult, Operation } from '../types';
+import { Exchange, ExchangeResult, Operation, OperationType } from '../types';
 
 type ResultCache = Map<string, ExchangeResult>;
 interface OperationCache {
@@ -30,7 +30,7 @@ export const cacheExchange: Exchange = ({ forward, subject }) => {
 
     const [cacheOps$, forwardOps$] = partition<Operation>(operation => {
       return (
-        operation.operationName === 'query' && resultCache.has(operation.key)
+        operation.operationName === OperationType.Query && resultCache.has(operation.key)
       );
     })(sharedOps$);
 
@@ -42,9 +42,9 @@ export const cacheExchange: Exchange = ({ forward, subject }) => {
       map(mapTypeNames),
       forward,
       tap(response => {
-        if (response.operation.operationName === 'mutation') {
+        if (response.operation.operationName === OperationType.Mutation) {
           handleAfterMutation(response);
-        } else if (response.operation.operationName === 'query') {
+        } else if (response.operation.operationName === OperationType.Query) {
           handleAfterQuery(response);
         }
       })
diff --git a/src/exchanges/dedup.ts b/src/exchanges/dedup.ts
index 598cd1f6..b0db568c 100644
--- a/src/exchanges/dedup.ts
+++ b/src/exchanges/dedup.ts
@@ -1,5 +1,5 @@
 import { filter, tap } from 'rxjs/operators';
-import { Exchange } from '../types';
+import { Exchange, OperationType } from '../types';
 
 /** A default exchange for debouncing GraphQL requests. */
 export const dedupeExchange: Exchange = ({ forward }) => {
@@ -9,7 +9,7 @@ export const dedupeExchange: Exchange = ({ forward }) => {
     forward(
       ops$.pipe(
         filter(({ operationName, key }) => {
-          if (operationName !== 'query') {
+          if (operationName !== OperationType.Query) {
             return true;
           }
 
diff --git a/src/exchanges/fetch.ts b/src/exchanges/fetch.ts
index 7c1d88e7..82ebd4d5 100755
--- a/src/exchanges/fetch.ts
+++ b/src/exchanges/fetch.ts
@@ -1,14 +1,14 @@
 import { Observable } from 'rxjs';
 import { mergeMap } from 'rxjs/operators';
 import { CombinedError } from '../lib';
-import { Exchange, ExchangeResult, Operation } from '../types';
+import { Exchange, ExchangeResult, Operation, OperationType } from '../types';
 
 /** A default exchange for fetching GraphQL requests. */
 export const fetchExchange: Exchange = () => {
   return ops$ =>
     ops$.pipe(
       mergeMap(operation => {
-        if (operation.operationName === 'subscription') {
+        if (operation.operationName === OperationType.Subscription) {
           throw new Error(
             'Received a subscription operation in the httpExchange. You are probably trying to create a subscription. Have you added a subscriptionExchange?'
           );
diff --git a/src/exchanges/index.ts b/src/exchanges/index.ts
index 76196e1c..bb6ce80a 100644
--- a/src/exchanges/index.ts
+++ b/src/exchanges/index.ts
@@ -1,3 +1,4 @@
 export { cacheExchange } from './cache';
+export { subscriptionExchange } from './subscription';
 export { dedupeExchange } from './dedup';
 export { fetchExchange } from './fetch';
diff --git a/src/exchanges/subscription.ts b/src/exchanges/subscription.ts
new file mode 100644
index 00000000..b9cf8872
--- /dev/null
+++ b/src/exchanges/subscription.ts
@@ -0,0 +1,66 @@
+import { parse } from 'graphql/language/parser';
+import { merge, Observable, Observer, of } from 'rxjs';
+import { mergeMap, partition } from 'rxjs/operators';
+import { Exchange, ExchangeResult, Operation } from '../types';
+
+function extractOperationName(operation: Operation): Operation {
+  const doc = parse(operation.query);
+
+  // TODO: How do we enforce this? I feel like this has to be stable given the spec?
+  // this is grabbing the first selection from the subscription query. Which is required to only be 1.
+  // and backend servers require operationName to be the subscription name
+  const operationName =
+    // @ts-ignore
+    doc.definitions[0].selectionSet.selections[0].name.value;
+
+  return {
+    ...operation,
+    operationName,
+  };
+}
+const liveSubscriptions = new Map<string, () => void>();
+
+export const subscriptionExchange: Exchange = ({ forward }) => {
+  const handleSubscription = (operation: Operation) => {
+    if (operation.context.unsubscribe) {
+      return new Observable<ExchangeResult>(observer => {
+        const unsubscribe = liveSubscriptions.get(operation.query);
+        // clear away queries
+        liveSubscriptions.delete(operation.query);
+
+        if (unsubscribe) {
+          unsubscribe();
+        }
+
+        observer.complete();
+      });
+    }
+
+    return new Observable<ExchangeResult>(observer => {
+      const { unsubscribe } = operation.context.forwardSubscription(
+        extractOperationName(operation),
+        observer
+      );
+
+      liveSubscriptions.set(operation.query, unsubscribe);
+    });
+  };
+
+  return ops$ => {
+    return ops$.pipe(
+      source$ =>
+        of(
+          partition<Operation>(op => op.operationName === 'subscription')(
+            source$
+          )
+        ),
+      // @ts-ignore
+      mergeMap(([subscriptions$, operations$]) =>
+        merge(
+          subscriptions$.pipe(mergeMap(handleSubscription)),
+          operations$.pipe(forward)
+        )
+      )
+    );
+  };
+};
diff --git a/src/index.ts b/src/index.ts
index ace7e92d..e2af182a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,7 +3,13 @@ export {
   CombinedError,
   createQuery,
   createMutation,
+  createSubscription,
   createClient,
 } from './lib';
-export { cacheExchange, dedupeExchange, fetchExchange } from './exchanges';
+export {
+  cacheExchange,
+  subscriptionExchange,
+  dedupeExchange,
+  fetchExchange,
+} from './exchanges';
 export * from './types';
diff --git a/src/lib/client.ts b/src/lib/client.ts
index 579e47c8..25a959e6 100755
--- a/src/lib/client.ts
+++ b/src/lib/client.ts
@@ -1,8 +1,13 @@
-import { Observable, Subject, Subscription } from 'rxjs';
+import { Observable, Subject, Subscription as RxSubscription } from 'rxjs';
 import { filter, publish, take } from 'rxjs/operators';
 import * as uuid from 'uuid';
-import { cacheExchange, dedupeExchange, fetchExchange } from '../exchanges';
-import { hashString } from '../lib';
+import {
+  cacheExchange,
+  dedupeExchange,
+  fetchExchange,
+  subscriptionExchange,
+} from '../exchanges';
+import { CombinedError, hashString } from '../lib';
 import {
   Client,
   ClientInstance,
@@ -12,17 +17,25 @@ import {
   ExchangeResult,
   Mutation,
   Operation,
+  OperationType,
   Query,
+  Subscription,
+  SubscriptionStreamUpdate,
 } from '../types';
 
-const defaultExchanges = [dedupeExchange, cacheExchange, fetchExchange];
+const defaultExchanges = [
+  subscriptionExchange,
+  dedupeExchange,
+  cacheExchange,
+  fetchExchange,
+];
 
 /** Function for creating an application wide [Client]{@link Client}. */
 export const createClient = (opts: ClientOptions): Client => {
   /** Cache of all instance subscriptions */
-  const subscriptions = new Map<string, Map<string, Subscription>>();
+  const subscriptions = new Map<string, Map<string, RxSubscription>>();
+  const querySubscriptions = new Map<string, Subject<Operation>>();
 
-  /** Main subject for emitting operations */
   const subject = new Subject<Operation>();
 
   /** Main subject stream for listening to operation responses */
@@ -42,9 +55,9 @@ export const createClient = (opts: ClientOptions): Client => {
 
   /** Convert a query to an operation type */
   const createOperation = (
-    type: string,
+    type: OperationType,
     query: Query,
-    force: boolean = false
+    options: object = {}
   ) => ({
     ...query,
     key: hashString(JSON.stringify(query)),
@@ -52,11 +65,13 @@ export const createClient = (opts: ClientOptions): Client => {
     context: {
       url: opts.url,
       fetchOptions,
-      force,
+      forwardSubscription: opts.forwardSubscription,
+      ...options,
     },
   });
 
-  const executeOperation = (operation: Operation) => subject.next(operation);
+  const executeOperation = (operation: Operation, sub: Subject<Operation>) =>
+    sub.next(operation);
 
   /** Function to manage subscriptions on a Consumer by Consumer basis */
   const createInstance = (
@@ -64,32 +79,31 @@ export const createClient = (opts: ClientOptions): Client => {
   ): ClientInstance => {
     const id = uuid.v4();
 
-    const getSubscriptions = () =>
+    const getRxSubscriptions = () =>
       subscriptions.has(id)
-        ? subscriptions.get(id)
-        : new Map<string, Subscription>();
+        ? (subscriptions.get(id) as Map<string, RxSubscription>)
+        : new Map<string, RxSubscription>();
 
-    const updateSubscriptions = (subs: Map<string, Subscription>) =>
+    const updateRxSubscriptions = (subs: Map<string, RxSubscription>) =>
       subscriptions.set(id, subs);
 
     const executeQuery = (query: Query, force?: boolean) => {
-      const operation = createOperation('query', query, force);
+      const operation = createOperation(OperationType.Query, query, { force });
 
       const outboundKey = `${operation.key}-out`;
       const inboundKey = `${operation.key}-in`;
 
-      const instanceSubscriptions = new Map(); // getSubscriptions();
-
-      if (!instanceSubscriptions.has(outboundKey)) {
+      const instanceRxSubscriptions = getRxSubscriptions();
+      if (!instanceRxSubscriptions.has(outboundKey)) {
         /** Notify component when fetching query is occurring */
         const outboundSub = subject
           .pipe(filter(newOp => newOp.key === operation.key))
           .subscribe(instanceOpts.onChange({ fetching: true }));
 
-        instanceSubscriptions.set(outboundKey, outboundSub);
+        instanceRxSubscriptions.set(outboundKey, outboundSub);
       }
 
-      if (!instanceSubscriptions.has(inboundKey)) {
+      if (!instanceRxSubscriptions.has(inboundKey)) {
         /** Notify component when query operation has returned */
         const inboundSub = subject$
           .pipe(filter(result => result.operation.key === operation.key))
@@ -100,15 +114,15 @@ export const createClient = (opts: ClientOptions): Client => {
               fetching: false,
             });
           });
-        instanceSubscriptions.set(inboundKey, inboundSub);
+        instanceRxSubscriptions.set(inboundKey, inboundSub);
       }
 
-      updateSubscriptions(instanceSubscriptions);
-      executeOperation(operation);
+      updateRxSubscriptions(instanceRxSubscriptions);
+      executeOperation(operation, subject);
     };
 
     const executeMutation = (query: Mutation) => {
-      const operation = createOperation('mutation', query);
+      const operation = createOperation(OperationType.Mutation, query);
       subject
         .pipe(
           take(1),
@@ -116,12 +130,58 @@ export const createClient = (opts: ClientOptions): Client => {
         )
         .subscribe();
 
-      executeOperation(operation);
+      executeOperation(operation, subject);
     };
 
-    const unsubscribe = () => {
-      const subs = getSubscriptions();
+    const executeSubscription = (query: Subscription) => {
+      const operation = createOperation(OperationType.Subscription, query);
+      const subscriptionSubject = new Subject<Operation>();
 
+      querySubscriptions.set(query.query, subscriptionSubject);
+
+      /** Notify component when subscription operation has been given a value */
+      subscriptionSubject
+        .pipe(
+          pipeExchanges(
+            opts.exchanges !== undefined ? opts.exchanges : defaultExchanges,
+            subject
+          )
+        )
+        .subscribe((response: SubscriptionStreamUpdate) => {
+          instanceOpts.onSubscriptionUpdate({
+            data: response.data.data,
+            error: Array.isArray(response.data.errors)
+              ? new CombinedError({
+                  graphQLErrors: response.data.errors,
+                  response,
+                })
+              : response.error,
+          });
+        });
+
+      executeOperation(operation, subscriptionSubject);
+    };
+
+    const executeUnsubscribeSubscription = (query: Subscription) => {
+      const operation = createOperation(OperationType.Subscription, query, {
+        unsubscribe: true,
+      });
+
+      // remove cached rx subscription
+      const activeSubscriptionSubject = querySubscriptions.get(query.query);
+
+      if (activeSubscriptionSubject !== undefined) {
+        querySubscriptions.delete(query.query);
+        executeOperation(operation, activeSubscriptionSubject);
+
+        // This removes the previous existence of a subscription from being part of the subject
+        // without this, any unsubscribe -> subscribe will have an n^2 effect.
+        activeSubscriptionSubject.unsubscribe();
+      }
+    };
+
+    const unsubscribe = () => {
+      const subs = getRxSubscriptions();
       if (subs === undefined) {
         return;
       }
@@ -132,6 +192,8 @@ export const createClient = (opts: ClientOptions): Client => {
     return {
       executeQuery,
       executeMutation,
+      executeSubscription,
+      executeUnsubscribeSubscription,
       unsubscribe,
     };
   };
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 2ac491f2..668838eb 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -1,5 +1,5 @@
 export { createClient } from './client';
 export { CombinedError } from './error';
 export { hashString } from './hash';
-export { createQuery, createMutation } from './query';
+export { createQuery, createMutation, createSubscription } from './query';
 export { formatTypeNames, gankTypeNamesFromResponse } from './typenames';
diff --git a/src/lib/query.ts b/src/lib/query.ts
index 68a2e371..7693fff0 100644
--- a/src/lib/query.ts
+++ b/src/lib/query.ts
@@ -6,3 +6,6 @@ export const createQuery = (q: string, vars?: any) => ({
 
 /** Create a [Mutation]{@link Mutation} from a mutation query. */
 export const createMutation = createQuery;
+
+/** Create a [Subscription]{@link Subscription} from a subscription query. */
+export const createSubscription = createQuery;
diff --git a/src/lib/typenames.ts b/src/lib/typenames.ts
index 6d53466d..3ed42948 100755
--- a/src/lib/typenames.ts
+++ b/src/lib/typenames.ts
@@ -90,6 +90,7 @@ function addTypenameToDocument(doc: DocumentNode) {
 // Adds __typename fields to a GraphQL query string
 export function formatTypeNames(query: string): string {
   const doc = parse(query);
+
   return print(addTypenameToDocument(doc));
 }
 
diff --git a/src/types.ts b/src/types.ts
index 15d0125b..fc54e626 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,4 +1,4 @@
-import { Observable, Subject } from 'rxjs';
+import { Observable, Subject, Subscriber } from 'rxjs';
 import { ClientState } from './components';
 import { CombinedError } from './lib';
 
@@ -13,12 +13,21 @@ export interface Query {
 /** A Graphql mutation. */
 export type Mutation = Query;
 
-/** A Graphql [query]{@link Query} or [mutation]{@link Mutation} accompanied with metadata. */
+/** A Graphql subscription. */
+export type Subscription = Query;
+
+export enum OperationType {
+  Subscription = 'subscription',
+  Query = 'query',
+  Mutation = 'mutation',
+}
+
+/** A [query]{@link Query} or [mutation]{@link Mutation} with additional metadata for use during transmission. */
 export interface Operation extends Query {
   /** Unique identifier of the operation. */
   key: string;
   /** The type of Grapqhql operation being executed. */
-  operationName: string;
+  operationName: OperationType;
   /** Additional metadata passed to [exchange]{@link Exchange} functions. */
   context: {
     [key: string]: any;
@@ -75,6 +84,11 @@ export interface ClientOptions {
   fetchOptions?: RequestInit | (() => RequestInit);
   /** An ordered array of Exchanges. */
   exchanges?: Exchange[];
+  /** forwards a subscription to users transport library. Must return an unsubscribe handler */
+  forwardSubscription?: (
+    operation: Operation,
+    observable: Subscriber<ExchangeResult>
+  ) => { unsubscribe: () => void };
 }
 
 /** The URQL applicaiton-wide client library. */
@@ -89,6 +103,10 @@ export interface ClientInstance {
   executeQuery: (query: Query, force?: boolean) => void;
   /** Executes a given mutation. */
   executeMutation: (mutation: Mutation, force?: boolean) => void;
+  /** Executes a given mutation. */
+  executeSubscription: (subscription: Subscription) => void;
+  /** Executes an unsubscribe for a subscription. */
+  executeUnsubscribeSubscription: (subscription: Subscription) => void;
   /** Removes any [subscriptions]{@link Subscription} created by the client instance. */
   unsubscribe: () => void;
 }
@@ -103,10 +121,21 @@ export interface StreamUpdate {
   fetching: boolean;
 }
 
+/** A new response/update from the [client]{@link Client} subscription stream. */
+export interface SubscriptionStreamUpdate {
+  /** Any data returned from a [GraphQL]{@link GraphQL} query. */
+  data?: ExchangeResult['data'];
+  /** Any errors returned from a [GraphQL]{@link GraphQL} query. */
+  error?: ExchangeResult['error'];
+}
+
 /** Arguments for creating a client instance. */
 export interface CreateClientInstanceOpts {
   /** A callback function for when changes occur. */
   onChange: (data: StreamUpdate) => any;
+
+  /** A callback function for when subscriptions return new data. */
+  onSubscriptionUpdate: (data: SubscriptionStreamUpdate) => any;
 }
 
 /** An error from the GraphQL client. */
diff --git a/yarn.lock b/yarn.lock
index dbfc0285..bfeb6403 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,18 @@
 # yarn lockfile v1
 
 
+"@apollographql/apollo-tools@^0.2.6":
+  version "0.2.9"
+  resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.2.9.tgz#1e20999d11728ef47f8f812f2be0426b5dde1a51"
+  integrity sha512-AEIQwPkS0QLbkpb6WyRhV4aOMxuErasp47ABv5niDKOasQH8mrD8JSGKJAHuQxVe4kB8DE9sLRoc5qeQ0KFCHA==
+  dependencies:
+    apollo-env "0.2.5"
+
+"@apollographql/graphql-playground-html@^1.6.6":
+  version "1.6.6"
+  resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.6.tgz#022209e28a2b547dcde15b219f0c50f47aa5beb3"
+  integrity sha512-lqK94b+caNtmKFs5oUVXlSpN3sm5IXZ+KfhMxOtr0LR2SqErzkoJilitjDvJ1WbjHlxLI7WtCjRmOLdOGJqtMQ==
+
 "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35":
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
@@ -32,6 +44,59 @@
     log-update "^2.3.0"
     strip-ansi "^3.0.1"
 
+"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
+  integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78=
+
+"@protobufjs/base64@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
+  integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
+
+"@protobufjs/codegen@^2.0.4":
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
+  integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
+
+"@protobufjs/eventemitter@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
+  integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A=
+
+"@protobufjs/fetch@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
+  integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=
+  dependencies:
+    "@protobufjs/aspromise" "^1.1.1"
+    "@protobufjs/inquire" "^1.1.0"
+
+"@protobufjs/float@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
+  integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=
+
+"@protobufjs/inquire@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
+  integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=
+
+"@protobufjs/path@^1.1.2":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
+  integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=
+
+"@protobufjs/pool@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
+  integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=
+
+"@protobufjs/utf8@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
+  integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
+
 "@samverschueren/stream-to-observable@^0.3.0":
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
@@ -39,11 +104,40 @@
   dependencies:
     any-observable "^0.3.0"
 
+"@types/accepts@^1.3.5":
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
+  integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==
+  dependencies:
+    "@types/node" "*"
+
+"@types/body-parser@*", "@types/body-parser@1.17.0":
+  version "1.17.0"
+  resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c"
+  integrity sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==
+  dependencies:
+    "@types/connect" "*"
+    "@types/node" "*"
+
 "@types/cheerio@*":
   version "0.22.10"
   resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.10.tgz#780d552467824be4a241b29510a7873a7432c4a6"
   integrity sha512-fOM/Jhv51iyugY7KOBZz2ThfT1gwvsGCfWxpLpZDgkGjpEO4Le9cld07OdskikLjDUQJ43dzDaVRSFwQlpdqVg==
 
+"@types/connect@*":
+  version "3.4.32"
+  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28"
+  integrity sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==
+  dependencies:
+    "@types/node" "*"
+
+"@types/cors@^2.8.4":
+  version "2.8.4"
+  resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.4.tgz#50991a759a29c0b89492751008c6af7a7c8267b0"
+  integrity sha512-ipZjBVsm2tF/n8qFGOuGBkUij9X9ZswVi9G3bx/6dz7POpVa6gVHcj1wsX/LVEn9MMF41fxK/PnZPPoTD1UFPw==
+  dependencies:
+    "@types/express" "*"
+
 "@types/enzyme@^3.1.15":
   version "3.1.15"
   resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.15.tgz#fc9a9695ba9f90cd50c4967e64a8c66ec96913d1"
@@ -57,6 +151,29 @@
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
   integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
 
+"@types/events@*":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
+  integrity sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==
+
+"@types/express-serve-static-core@*":
+  version "4.16.0"
+  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz#fdfe777594ddc1fe8eb8eccce52e261b496e43e7"
+  integrity sha512-lTeoCu5NxJU4OD9moCgm0ESZzweAx0YqsAcab6OB0EB3+As1OaHtKnaGJvcngQxYsi9UNv0abn4/DRavrRxt4w==
+  dependencies:
+    "@types/events" "*"
+    "@types/node" "*"
+    "@types/range-parser" "*"
+
+"@types/express@*", "@types/express@4.16.0":
+  version "4.16.0"
+  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.16.0.tgz#6d8bc42ccaa6f35cf29a2b7c3333cb47b5a32a19"
+  integrity sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w==
+  dependencies:
+    "@types/body-parser" "*"
+    "@types/express-serve-static-core" "*"
+    "@types/serve-static" "*"
+
 "@types/graphql@^14.0.3":
   version "14.0.3"
   resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.0.3.tgz#389e2e5b83ecdb376d9f98fae2094297bc112c1c"
@@ -67,16 +184,36 @@
   resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.10.tgz#4897974cc317bf99d4fe6af1efa15957fa9c94de"
   integrity sha512-DC8xTuW/6TYgvEg3HEXS7cu9OijFqprVDXXiOcdOKZCU/5PJNLZU37VVvmZHdtMiGOa8wAA/We+JzbdxFzQTRQ==
 
+"@types/long@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef"
+  integrity sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==
+
+"@types/mime@*":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b"
+  integrity sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==
+
 "@types/node@*":
   version "10.12.10"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.10.tgz#4fa76e6598b7de3f0cb6ec3abacc4f59e5b3a2ce"
   integrity sha512-8xZEYckCbUVgK8Eg7lf5Iy4COKJ5uXlnIOnePN0WUwSQggy9tolM+tDJf7wMOnT/JT/W9xDYIaYggt3mRV2O5w==
 
+"@types/node@^10.1.0":
+  version "10.12.18"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
+  integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
+
 "@types/prop-types@*":
   version "15.5.6"
   resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.6.tgz#9c03d3fed70a8d517c191b7734da2879b50ca26c"
   integrity sha512-ZBFR7TROLVzCkswA3Fmqq+IIJt62/T7aY/Dmz+QkU7CaW2QFqAitCE8Ups7IzmGhcN1YWMBT4Qcoc07jU9hOJQ==
 
+"@types/range-parser@*":
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
+  integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
+
 "@types/react-dom@^16.0.3":
   version "16.0.11"
   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.11.tgz#bd10ccb0d9260343f4b9a49d4f7a8330a5c1f081"
@@ -92,6 +229,14 @@
     "@types/prop-types" "*"
     csstype "^2.2.0"
 
+"@types/serve-static@*":
+  version "1.13.2"
+  resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48"
+  integrity sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==
+  dependencies:
+    "@types/express-serve-static-core" "*"
+    "@types/mime" "*"
+
 "@types/uuid@^3.4.3":
   version "3.4.4"
   resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5"
@@ -99,6 +244,14 @@
   dependencies:
     "@types/node" "*"
 
+"@types/ws@^6.0.0":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.1.tgz#ca7a3f3756aa12f62a0a62145ed14c6db25d5a28"
+  integrity sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==
+  dependencies:
+    "@types/events" "*"
+    "@types/node" "*"
+
 "@webassemblyjs/ast@1.7.11":
   version "1.7.11"
   resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace"
@@ -387,6 +540,48 @@ anymatch@^2.0.0:
     micromatch "^3.1.4"
     normalize-path "^2.1.1"
 
+apollo-cache-control@0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.4.0.tgz#fec343e6ec95aa4f1b88e07e62f067bee0c48397"
+  integrity sha512-WuriaNQIugTE8gYwfBWWCbbQTSKul/cV4JMi5UgqNIUvjHvnKZQLKbt5uYWow6QQNMkLT9hey8QPYkWpogkeSA==
+  dependencies:
+    apollo-server-env "2.2.0"
+    graphql-extensions "0.4.0"
+
+apollo-datasource@0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.2.1.tgz#3ecef4efe64f7a04a43862f32027d38ac09e142c"
+  integrity sha512-r185+JTa5KuF1INeTAk7AEP76zwMN6c8Ph1lmpzJMNwBUEzTGnLClrccCskCBx4SxfnkdKbuQdwn9JwCJUWrdg==
+  dependencies:
+    apollo-server-caching "0.2.1"
+    apollo-server-env "2.2.0"
+
+apollo-engine-reporting-protobuf@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.2.0.tgz#2aaf4d2eddefe7924d469cf1135267bc0deadf73"
+  integrity sha512-qI+GJKN78UMJ9Aq/ORdiM2qymZ5yswem+/VDdVFocq+/e1QqxjnpKjQWISkswci5+WtpJl9SpHBNxG98uHDKkA==
+  dependencies:
+    protobufjs "^6.8.6"
+
+apollo-engine-reporting@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-0.2.0.tgz#e71816b1f46e782f8538c5a118148d4c0e628e25"
+  integrity sha512-Q6FfVb10v/nrv8FaFsPjIYlWh62jaYav3LuMgM9PsHWGK/zRQFXOEwLxcY2UCvG7O1moxF3XGmfBhMgo54py+Q==
+  dependencies:
+    apollo-engine-reporting-protobuf "0.2.0"
+    apollo-server-env "2.2.0"
+    async-retry "^1.2.1"
+    graphql-extensions "0.4.0"
+    lodash "^4.17.10"
+
+apollo-env@0.2.5:
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.2.5.tgz#162c785bccd2aea69350a7600fab4b7147fc9da5"
+  integrity sha512-Gc7TEbwCl7jJVutnn8TWfzNSkrrqyoo0DP92BQJFU9pZbJhpidoXf2Sw1YwOJl82rRKH3ujM3C8vdZLOgpFcFA==
+  dependencies:
+    core-js "^3.0.0-beta.3"
+    node-fetch "^2.2.0"
+
 apollo-link@^1.2.3:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.4.tgz#ab4d21d2e428db848e88b5e8f4adc717b19c954b"
@@ -395,6 +590,83 @@ apollo-link@^1.2.3:
     apollo-utilities "^1.0.0"
     zen-observable-ts "^0.8.11"
 
+apollo-server-caching@0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.2.1.tgz#7e67f8c8cac829e622b394f0fb82579cabbeadfd"
+  integrity sha512-+U9F3X297LL8Gqy6ypfDNEv/DfV/tDht9Dr2z3AMaEkNW1bwO6rmdDL01zYxDuVDVq6Z3qSiNCSO2pXE2F0zmA==
+  dependencies:
+    lru-cache "^5.0.0"
+
+apollo-server-core@2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.3.1.tgz#cbdc0020a0dfecf2220cf5062dbb304fdf56edf2"
+  integrity sha512-8jMWYOQIZi9mDJlHe2rXg8Cp4xKYogeRu23jkcNy+k5UjZL+eO+kHXbNFiTaP4HLYYEpe2XE3asxp6q5YUEQeQ==
+  dependencies:
+    "@apollographql/apollo-tools" "^0.2.6"
+    "@apollographql/graphql-playground-html" "^1.6.6"
+    "@types/ws" "^6.0.0"
+    apollo-cache-control "0.4.0"
+    apollo-datasource "0.2.1"
+    apollo-engine-reporting "0.2.0"
+    apollo-server-caching "0.2.1"
+    apollo-server-env "2.2.0"
+    apollo-server-errors "2.2.0"
+    apollo-server-plugin-base "0.2.1"
+    apollo-tracing "0.4.0"
+    graphql-extensions "0.4.1"
+    graphql-subscriptions "^1.0.0"
+    graphql-tag "^2.9.2"
+    graphql-tools "^4.0.0"
+    graphql-upload "^8.0.2"
+    json-stable-stringify "^1.0.1"
+    lodash "^4.17.10"
+    subscriptions-transport-ws "^0.9.11"
+    ws "^6.0.0"
+
+apollo-server-env@2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.2.0.tgz#5eec5dbf46581f663fd6692b2e05c7e8ae6d6034"
+  integrity sha512-wjJiI5nQWPBpNmpiLP389Ezpstp71szS6DHAeTgYLb/ulCw3CTuuA+0/E1bsThVWiQaDeHZE0sE3yI8q2zrYiA==
+  dependencies:
+    node-fetch "^2.1.2"
+    util.promisify "^1.0.0"
+
+apollo-server-errors@2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.2.0.tgz#5b452a1d6ff76440eb0f127511dc58031a8f3cb5"
+  integrity sha512-gV9EZG2tovFtT1cLuCTavnJu2DaKxnXPRNGSTo+SDI6IAk6cdzyW0Gje5N2+3LybI0Wq5KAbW6VLei31S4MWmg==
+
+apollo-server-express@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.3.1.tgz#0598e2fa0a0d9e6eb570c0bb6ce65c31810a9c09"
+  integrity sha512-J+rObr4GdT/5j6qTByUJoSvZSjTAX/7VqIkr2t+GxwcVUFGet2MdOHuV6rtWKc8CRgvVKfKN6iBrb2EOFcp2LQ==
+  dependencies:
+    "@apollographql/graphql-playground-html" "^1.6.6"
+    "@types/accepts" "^1.3.5"
+    "@types/body-parser" "1.17.0"
+    "@types/cors" "^2.8.4"
+    "@types/express" "4.16.0"
+    accepts "^1.3.5"
+    apollo-server-core "2.3.1"
+    body-parser "^1.18.3"
+    cors "^2.8.4"
+    graphql-subscriptions "^1.0.0"
+    graphql-tools "^4.0.0"
+    type-is "^1.6.16"
+
+apollo-server-plugin-base@0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.2.1.tgz#d08c9576f7f11ab6e212f352d482faaa4059a31e"
+  integrity sha512-497NIY9VWRYCrMSkgR11IrIUO4Fsy6aGgnpOJoTdLQAnkDD9SJDSRzwKj4gypUoTT2unfKDng4eMxXVZlHvjOw==
+
+apollo-tracing@0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.4.0.tgz#4b939063f4292422ac5a3564b76d1d88dec0a916"
+  integrity sha512-BlM8iQUQva4fm0xD/pLwkcz0degfB9a/aAn4k4cK36eLVD8XUkl7ptEB0c+cwcj7tOYpV1r5QX1XwdayBzlHSg==
+  dependencies:
+    apollo-server-env "2.2.0"
+    graphql-extensions "0.4.0"
+
 apollo-utilities@^1.0.0, apollo-utilities@^1.0.1:
   version "1.0.26"
   resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.26.tgz#589c66bf4d16223531351cf667a230c787def1da"
@@ -555,6 +827,13 @@ async-limiter@~1.0.0:
   resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
   integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==
 
+async-retry@^1.2.1:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.2.3.tgz#a6521f338358d322b1a0012b79030c6f411d1ce0"
+  integrity sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q==
+  dependencies:
+    retry "0.12.0"
+
 async@^1.4.2, async@^1.5.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
@@ -815,6 +1094,11 @@ babylon@^6.15.0, babylon@^6.18.0:
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
   integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
 
+backo2@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
+  integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
+
 balanced-match@^0.4.2:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
@@ -890,7 +1174,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
   integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
 
-body-parser@1.18.3:
+body-parser@1.18.3, body-parser@^1.18.3:
   version "1.18.3"
   resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4"
   integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=
@@ -1176,6 +1460,14 @@ bundlesize@^0.17.0:
     prettycli "^1.4.3"
     read-pkg-up "^3.0.0"
 
+busboy@^0.2.14:
+  version "0.2.14"
+  resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
+  integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=
+  dependencies:
+    dicer "0.2.5"
+    readable-stream "1.1.x"
+
 bytes@3.0.0, bytes@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@@ -1620,7 +1912,7 @@ content-disposition@0.5.2:
   resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
   integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ=
 
-content-type@^1.0.4, content-type@~1.0.4:
+content-type@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
@@ -1669,6 +1961,11 @@ core-js@^2.4.0, core-js@^2.5.0:
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
   integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==
 
+core-js@^3.0.0-beta.3:
+  version "3.0.0-beta.7"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.0-beta.7.tgz#d8bc953adc4805e81dbfc73c18050f6ca78d547b"
+  integrity sha512-Tf2uksQ1zuzoLbjWhEej9/iSAtjwtqzuXAGUidO7PNuUN2uPCCnJoR6YmIO3Ibq6vuGNzH6wt+3pLHrl7wEIYw==
+
 core-util-is@1.0.2, core-util-is@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -2271,6 +2568,14 @@ detect-node@^2.0.3:
   resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
   integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
 
+dicer@0.2.5:
+  version "0.2.5"
+  resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f"
+  integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=
+  dependencies:
+    readable-stream "1.1.x"
+    streamsearch "0.1.2"
+
 diff@^3.2.0:
   version "3.5.0"
   resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
@@ -2661,7 +2966,7 @@ etag@~1.8.1:
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
   integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
 
-eventemitter3@^3.0.0:
+eventemitter3@^3.0.0, eventemitter3@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
   integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==
@@ -2786,16 +3091,6 @@ expect@^23.6.0:
     jest-message-util "^23.4.0"
     jest-regex-util "^23.3.0"
 
-express-graphql@^0.7.1:
-  version "0.7.1"
-  resolved "https://registry.yarnpkg.com/express-graphql/-/express-graphql-0.7.1.tgz#6c7712ee966c3aba1930e064ea4c8181e56fd3ef"
-  integrity sha512-YpheAqTbSKpb5h57rV2yu2dPNUBi4FvZDspZ5iEV3ov34PBRgnM4lEBkv60+vZRJ6SweYL14N8AGYdov7g6ooQ==
-  dependencies:
-    accepts "^1.3.5"
-    content-type "^1.0.4"
-    http-errors "^1.7.1"
-    raw-body "^2.3.3"
-
 express@^4.16.2:
   version "4.16.4"
   resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e"
@@ -3131,6 +3426,11 @@ from2@^2.1.0:
     inherits "^2.0.1"
     readable-stream "^2.0.0"
 
+fs-capacitor@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-1.0.1.tgz#ff9dbfa14dfaf4472537720f19c3088ed9278df0"
+  integrity sha512-XdZK0Q78WP29Vm3FGgJRhRhrBm51PagovzWtW2kJ3Q6cYJbGtZqWSGTSPwvtEkyjIirFd7b8Yes/dpOYjt4RRQ==
+
 fs-constants@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
@@ -3360,7 +3660,45 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
   integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
 
-graphql-tools@^4.0.3:
+graphql-extensions@0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.4.0.tgz#5857c7b7b9f20dbccbfd88730fffa5963b3c61ee"
+  integrity sha512-8TUgIIUVpXWOcqq9RdmTSHUrhc3a/s+saKv9cCl8TYWHK9vyJIdea7ZaSKHGDthZNcsN+C3LulZYRL3Ah8ukoA==
+  dependencies:
+    "@apollographql/apollo-tools" "^0.2.6"
+
+graphql-extensions@0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.4.1.tgz#92c49a8409ffbfb24559d7661ab60cc90d6086e4"
+  integrity sha512-Xei4rBxbsTHU6dYiq9y1xxbpRMU3+Os7yD3vXV5W4HbTaxRMizDmu6LAvV4oBEi0ttwICHARQjYTjDTDhHnxrQ==
+  dependencies:
+    "@apollographql/apollo-tools" "^0.2.6"
+
+graphql-playground-html@1.6.6:
+  version "1.6.6"
+  resolved "https://registry.yarnpkg.com/graphql-playground-html/-/graphql-playground-html-1.6.6.tgz#f4efe0915a01dc79154b836fe3c980cebb7b56d2"
+  integrity sha512-VfCnMK24BwOAGhFzjknlboK0qs92d+1sHUDGQUgIAjOsTSNWmqfgNkDZsONZqUajfuVjOYRd0PxCDkCkaJs7Rw==
+
+graphql-playground-middleware-express@^1.7.8:
+  version "1.7.8"
+  resolved "https://registry.yarnpkg.com/graphql-playground-middleware-express/-/graphql-playground-middleware-express-1.7.8.tgz#eb05f26f2b23084e4bb29ce41f2c5983ce870dfc"
+  integrity sha512-3wFOfsJGUtWJuGsA+jQhbVMYAI8x1f5noj4wyySPMhLOK13NiElmsNKrV1sUDb0DJaf5tfg72N0ULMfFQagy9A==
+  dependencies:
+    graphql-playground-html "1.6.6"
+
+graphql-subscriptions@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.0.0.tgz#475267694b3bd465af6477dbab4263a3f62702b8"
+  integrity sha512-+ytmryoHF1LVf58NKEaNPRUzYyXplm120ntxfPcgOBC7TnK7Tv/4VRHeh4FAR9iL+O1bqhZs4nkibxQ+OA5cDQ==
+  dependencies:
+    iterall "^1.2.1"
+
+graphql-tag@^2.9.2:
+  version "2.10.0"
+  resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.0.tgz#87da024be863e357551b2b8700e496ee2d4353ae"
+  integrity sha512-9FD6cw976TLLf9WYIUPCaaTpniawIjHWZSwIRZSjrfufJamcXbVVYfN2TWvJYbw0Xf2JjYbl1/f2+wDnBVw3/w==
+
+graphql-tools@^4.0.0:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.3.tgz#23b5cb52c519212b1b2e4630a361464396ad264b"
   integrity sha512-NNZM0WSnVLX1zIMUxu7SjzLZ4prCp15N5L2T2ro02OVyydZ0fuCnZYRnx/yK9xjGWbZA0Q58yEO//Bv/psJWrg==
@@ -3371,6 +3709,16 @@ graphql-tools@^4.0.3:
     iterall "^1.1.3"
     uuid "^3.1.0"
 
+graphql-upload@^8.0.2:
+  version "8.0.2"
+  resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.0.2.tgz#1c1f116f15b7f8485cf40ff593a21368f0f58856"
+  integrity sha512-u8a5tKPfJ0rU4MY+B3skabL8pEjMkm3tUzq25KBx6nT0yEWmqUO7Z5tdwvwYLFpkLwew94Gue0ARbZtar3gLTw==
+  dependencies:
+    busboy "^0.2.14"
+    fs-capacitor "^1.0.0"
+    http-errors "^1.7.1"
+    object-path "^0.11.4"
+
 graphql@^14.0.2:
   version "14.0.2"
   resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.0.2.tgz#7dded337a4c3fd2d075692323384034b357f5650"
@@ -4232,6 +4580,11 @@ is-wsl@^1.1.0:
   resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
   integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
 
+isarray@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+  integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
+
 isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -4337,7 +4690,7 @@ istanbul-reports@^1.5.1:
   dependencies:
     handlebars "^4.0.3"
 
-iterall@^1.1.3, iterall@^1.2.2:
+iterall@^1.1.3, iterall@^1.2.1, iterall@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7"
   integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==
@@ -4752,6 +5105,13 @@ json-schema@0.2.3:
   resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
   integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
 
+json-stable-stringify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
+  integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=
+  dependencies:
+    jsonify "~0.0.0"
+
 json-stringify-safe@~5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@@ -4781,6 +5141,11 @@ jsonfile@^4.0.0:
   optionalDependencies:
     graceful-fs "^4.1.6"
 
+jsonify@~0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+  integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
+
 jsprim@^1.2.2:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -5099,6 +5464,11 @@ loglevelnext@^1.0.1:
     es6-symbol "^3.1.1"
     object.assign "^4.1.0"
 
+long@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
+  integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
+
 loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@@ -5114,6 +5484,13 @@ lru-cache@^4.0.1, lru-cache@^4.1.3:
     pseudomap "^1.0.2"
     yallist "^2.1.2"
 
+lru-cache@^5.0.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+  integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+  dependencies:
+    yallist "^3.0.2"
+
 magic-string@^0.22.4:
   version "0.22.5"
   resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
@@ -5586,6 +5963,11 @@ node-fetch@^1.0.1:
     encoding "^0.1.11"
     is-stream "^1.0.1"
 
+node-fetch@^2.1.2, node-fetch@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5"
+  integrity sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==
+
 node-forge@0.7.5:
   version "0.7.5"
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df"
@@ -5858,6 +6240,11 @@ object-keys@^1.0.11, object-keys@^1.0.12:
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2"
   integrity sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==
 
+object-path@^0.11.4:
+  version "0.11.4"
+  resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949"
+  integrity sha1-NwrnUvvzfePqcKhhwju6iRVpGUk=
+
 object-visit@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
@@ -7050,6 +7437,25 @@ prop-types@^15.6.0, prop-types@^15.6.2:
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
+protobufjs@^6.8.6:
+  version "6.8.8"
+  resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c"
+  integrity sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==
+  dependencies:
+    "@protobufjs/aspromise" "^1.1.2"
+    "@protobufjs/base64" "^1.1.2"
+    "@protobufjs/codegen" "^2.0.4"
+    "@protobufjs/eventemitter" "^1.1.0"
+    "@protobufjs/fetch" "^1.1.0"
+    "@protobufjs/float" "^1.0.2"
+    "@protobufjs/inquire" "^1.1.0"
+    "@protobufjs/path" "^1.1.2"
+    "@protobufjs/pool" "^1.1.0"
+    "@protobufjs/utf8" "^1.1.0"
+    "@types/long" "^4.0.0"
+    "@types/node" "^10.1.0"
+    long "^4.0.0"
+
 proxy-addr@~2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93"
@@ -7215,7 +7621,7 @@ range-parser@^1.0.3, range-parser@~1.2.0:
   resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
   integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=
 
-raw-body@2.3.3, raw-body@^2.3.3:
+raw-body@2.3.3:
   version "2.3.3"
   resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3"
   integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==
@@ -7331,6 +7737,16 @@ read-pkg@^4.0.1:
     string_decoder "~1.1.1"
     util-deprecate "~1.0.1"
 
+readable-stream@1.1.x:
+  version "1.1.14"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
+  integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.1"
+    isarray "0.0.1"
+    string_decoder "~0.10.x"
+
 readable-stream@^3.0.6:
   version "3.0.6"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.0.6.tgz#351302e4c68b5abd6a2ed55376a7f9a25be3057a"
@@ -7585,6 +8001,11 @@ ret@~0.1.10:
   resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
   integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
 
+retry@0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
+  integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
+
 rgb-regex@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
@@ -8309,6 +8730,11 @@ stream-shift@^1.0.0:
   resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
   integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
 
+streamsearch@0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
+  integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
+
 strict-uri-encode@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
@@ -8365,6 +8791,11 @@ string_decoder@^1.0.0, string_decoder@^1.1.1:
   dependencies:
     safe-buffer "~5.1.0"
 
+string_decoder@~0.10.x:
+  version "0.10.31"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+  integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
+
 string_decoder@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
@@ -8431,6 +8862,17 @@ stylehacks@^4.0.0:
     postcss "^7.0.0"
     postcss-selector-parser "^3.0.0"
 
+subscriptions-transport-ws@^0.9.11, subscriptions-transport-ws@^0.9.15:
+  version "0.9.15"
+  resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.15.tgz#68a8b7ba0037d8c489fb2f5a102d1494db297d0d"
+  integrity sha512-f9eBfWdHsePQV67QIX+VRhf++dn1adyC/PZHP6XI5AfKnZ4n0FW+v5omxwdHVpd4xq2ZijaHEcmlQrhBY79ZWQ==
+  dependencies:
+    backo2 "^1.0.2"
+    eventemitter3 "^3.1.0"
+    iterall "^1.2.1"
+    symbol-observable "^1.0.4"
+    ws "^5.2.0"
+
 supports-color@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -8490,7 +8932,7 @@ svgo@^1.0.0:
     unquote "~1.1.1"
     util.promisify "~1.0.0"
 
-symbol-observable@^1.1.0:
+symbol-observable@^1.0.4, symbol-observable@^1.1.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
   integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
@@ -8786,7 +9228,7 @@ type-check@~0.3.2:
   dependencies:
     prelude-ls "~1.1.2"
 
-type-is@~1.6.16:
+type-is@^1.6.16, type-is@~1.6.16:
   version "1.6.16"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
   integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==
@@ -9316,6 +9758,13 @@ ws@^5.2.0:
   dependencies:
     async-limiter "~1.0.0"
 
+ws@^6.0.0:
+  version "6.1.2"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.2.tgz#3cc7462e98792f0ac679424148903ded3b9c3ad8"
+  integrity sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==
+  dependencies:
+    async-limiter "~1.0.0"
+
 xml-name-validator@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"


Test patch:
diff --git a/src/components/client.test.tsx b/src/components/client.test.tsx
index 620b1fca..a9d0af00 100755
--- a/src/components/client.test.tsx
+++ b/src/components/client.test.tsx
@@ -1,12 +1,14 @@
+import { mount, shallow } from 'enzyme';
 import React from 'react';
-import { shallow, mount } from 'enzyme';
+import { mutationGql, queryGql as query, subscriptionGql } from '../test-utils';
 import { UrqlClient } from './client';
-import { queryGql as query, mutationGql } from '../test-utils';
 
 const children = jest.fn().mockReturnValue(<h1>This is a child component</h1>);
 const clientInstance = {
   executeQuery: jest.fn(),
   executeMutation: jest.fn(),
+  executeSubscription: jest.fn(),
+  executeUnsubscribeSubscription: jest.fn(),
   unsubscribe: jest.fn(),
 };
 const client = { createInstance: jest.fn().mockReturnValue(clientInstance) };
@@ -21,6 +23,8 @@ beforeEach(() => {
   clientInstance.executeQuery.mockClear();
   clientInstance.executeMutation.mockClear();
   clientInstance.unsubscribe.mockClear();
+  clientInstance.executeSubscription.mockClear();
+  clientInstance.executeUnsubscribeSubscription.mockClear();
 });
 
 it('passes snapshot', () => {
@@ -44,6 +48,14 @@ describe('on creation', () => {
     expect(client.createInstance).toBeCalled();
   });
 
+  it('throws an error if subscriptions are passed without an updateSubscription prop', () => {
+    expect(() =>
+      shallow(
+        <UrqlClient client={client} subscriptions={[]} children={children} />
+      )
+    ).toThrow();
+  });
+
   describe('defaults', () => {
     it('passes snapshot', () => {
       shallow(
@@ -115,6 +127,25 @@ describe('componentDidMount', () => {
     wrapper.instance().componentDidMount();
     expect(clientInstance.executeQuery).toBeCalledWith(query);
   });
+
+  it('calls executeSubscription for each subscription it has', () => {
+    const subscriptions = [subscriptionGql, subscriptionGql];
+
+    shallow(
+      <UrqlClient
+        client={client}
+        query={query}
+        subscriptions={subscriptions}
+        updateSubscription={jest.fn()}
+      >
+        {children}
+      </UrqlClient>
+    );
+
+    expect(clientInstance.executeSubscription).toBeCalledTimes(
+      subscriptions.length
+    );
+  });
 });
 
 describe('componentDidUpdate', () => {
@@ -150,6 +181,30 @@ describe('componentDidUpdate', () => {
       Object.keys(newMutations)
     );
   });
+
+  it('subscriptions are unsubscribed, and new ones are subscribed', () => {
+    const subscriptions = [subscriptionGql, subscriptionGql];
+    const wrapper = mount(
+      <UrqlClient
+        client={client}
+        subscriptions={subscriptions}
+        updateSubscription={jest.fn()}
+      >
+        {children}
+      </UrqlClient>
+    );
+
+    // remove mock calls from mounting to get predictable test results.
+    clientInstance.executeSubscription.mockClear();
+    clientInstance.executeUnsubscribeSubscription.mockClear();
+
+    const newSubscriptions = [subscriptionGql];
+
+    wrapper.setProps({ subscriptions: newSubscriptions });
+
+    expect(clientInstance.executeUnsubscribeSubscription).toBeCalledTimes(2);
+    expect(clientInstance.executeSubscription).toBeCalledTimes(1);
+  });
 });
 
 describe('componentWillUnmount', () => {
@@ -164,6 +219,23 @@ describe('componentWillUnmount', () => {
     wrapper.instance().componentWillUnmount();
     expect(clientInstance.unsubscribe).toBeCalled();
   });
+
+  it('unsubscribes from subscriptions', () => {
+    const wrapper = shallow(
+      <UrqlClient
+        client={client}
+        subscriptions={[subscriptionGql]}
+        updateSubscription={jest.fn()}
+      >
+        {children}
+      </UrqlClient>
+    );
+
+    // @ts-ignore
+    wrapper.instance().componentWillUnmount();
+
+    expect(clientInstance.executeUnsubscribeSubscription).toBeCalledTimes(1);
+  });
 });
 
 describe('mutation functions', () => {
@@ -246,3 +318,44 @@ describe('on change from client', () => {
     expect(children).toBeCalledWith(expect.objectContaining(update));
   });
 });
+
+describe('on subscription change', () => {
+  it('delegates changes to the updateSubscription prop', () => {
+    const NOT_STATE_VALUE = 'NOT_STATE_VALUE';
+    const STATE_VALUE = 'STATE_VALUE';
+
+    const updateSubscription = (type, state, data) => {
+      if (type === 'test') {
+        state = data;
+      }
+
+      return state;
+    };
+
+    const wrapper = shallow(
+      <UrqlClient client={client} updateSubscription={updateSubscription}>
+        {children}
+      </UrqlClient>
+    );
+
+    // this data shape would not hit the state assignment in our updateSubscription updator
+    // so we assert that it didn't get set.
+    // @ts-ignore
+    wrapper.instance().onSubscriptionStreamUpdate({
+      data: {
+        nottest: NOT_STATE_VALUE,
+      },
+    });
+
+    expect(wrapper.state('data')).not.toBe(NOT_STATE_VALUE);
+
+    // @ts-ignore
+    wrapper.instance().onSubscriptionStreamUpdate({
+      data: {
+        test: STATE_VALUE,
+      },
+    });
+
+    expect(wrapper.state('data')).toBe(STATE_VALUE);
+  });
+});
diff --git a/src/exchanges/__snapshots__/subscription.test.ts.snap b/src/exchanges/__snapshots__/subscription.test.ts.snap
new file mode 100644
index 00000000..6188de55
--- /dev/null
+++ b/src/exchanges/__snapshots__/subscription.test.ts.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should return response data from fetch 1`] = `
+Object {
+  "data": Object {
+    "data": Object {
+      "user": 1200,
+    },
+  },
+}
+`;
diff --git a/src/exchanges/cache.test.ts b/src/exchanges/cache.test.ts
index b5a42090..23190df8 100755
--- a/src/exchanges/cache.test.ts
+++ b/src/exchanges/cache.test.ts
@@ -1,15 +1,15 @@
 import { Observable, Subject } from 'rxjs';
 import { map } from 'rxjs/operators';
-import { cacheExchange, afterMutation } from './cache';
 import {
-  queryOperation,
-  queryResponse,
   mutationOperation,
   mutationResponse,
-  subscriptionResponse,
+  queryOperation,
+  queryResponse,
   subscriptionOperation,
+  subscriptionResponse,
 } from '../test-utils';
 import { Operation } from '../types';
+import { afterMutation, cacheExchange } from './cache';
 
 const subject = {
   next: jest.fn(),
diff --git a/src/exchanges/fetch.test.ts b/src/exchanges/fetch.test.ts
index 8eeadc5e..71dea31c 100755
--- a/src/exchanges/fetch.test.ts
+++ b/src/exchanges/fetch.test.ts
@@ -1,8 +1,8 @@
 import { of } from 'rxjs';
 import { take } from 'rxjs/operators';
-import { fetchExchange } from './fetch';
-import { Exchange } from '../types';
 import { queryOperation, subscriptionOperation } from '../test-utils';
+import { Exchange } from '../types';
+import { fetchExchange } from './fetch';
 
 const fetch = (global as any).fetch as jest.Mock;
 const abort = jest.fn();
diff --git a/src/exchanges/subscription.test.ts b/src/exchanges/subscription.test.ts
new file mode 100644
index 00000000..e5e69676
--- /dev/null
+++ b/src/exchanges/subscription.test.ts
@@ -0,0 +1,59 @@
+import { of } from 'rxjs';
+import { take } from 'rxjs/operators';
+import { subscriptionExchange } from './subscription';
+import { subscriptionOperation } from '../test-utils';
+
+const response = {
+  data: {
+    data: {
+      user: 1200,
+    },
+  },
+};
+
+const args = {
+  forward: jest.fn(),
+  subject: {},
+} as any;
+
+const createOperation = res => {
+  const unsubscribe = jest.fn();
+
+  const operation = { ...subscriptionOperation };
+  operation.query.replace('subscribeToUser', `${Math.random() * Date.now()}`);
+  operation.context.forwardSubscription = jest.fn();
+
+  if (res) {
+    operation.context.forwardSubscription.mockImplementation((_, observer) => {
+      observer.next(res);
+      return { unsubscribe };
+    });
+  }
+
+  return { operation, unsubscribe };
+};
+
+it('should return response data from fetch', async () => {
+  const { operation } = createOperation(response);
+
+  const data = await subscriptionExchange(args)(of(operation))
+    .pipe(take(1))
+    .toPromise();
+
+  expect(data).toMatchSnapshot();
+});
+
+it('should unsubscribe from future data', async () => {
+  const { operation, unsubscribe } = createOperation(response);
+
+  // Subscribe
+  await subscriptionExchange(args)(of(operation))
+    .pipe(take(1))
+    .toPromise();
+
+  // Unsubscribe
+  operation.context.unsubscribe = true;
+  subscriptionExchange(args)(of(operation)).subscribe(() => void 0);
+
+  expect(unsubscribe).toBeCalled();
+});
diff --git a/src/lib/__snapshots__/client.test.ts.snap b/src/lib/__snapshots__/client.test.ts.snap
index a3860c3e..990f4248 100644
--- a/src/lib/__snapshots__/client.test.ts.snap
+++ b/src/lib/__snapshots__/client.test.ts.snap
@@ -10,6 +10,8 @@ exports[`createInstance passes snapshot 1`] = `
 Object {
   "executeMutation": [Function],
   "executeQuery": [Function],
+  "executeSubscription": [Function],
+  "executeUnsubscribeSubscription": [Function],
   "unsubscribe": [Function],
 }
 `;
diff --git a/src/lib/client.test.ts b/src/lib/client.test.ts
index fb461c78..1591cd2a 100755
--- a/src/lib/client.test.ts
+++ b/src/lib/client.test.ts
@@ -1,10 +1,17 @@
 /** NOTE: Testing in this file is designed to test both the client and it's interaction with default Exchanges */
-import { createClient } from './client';
-import { queryGql, mutationGql, queryResponse } from '../test-utils';
+import {
+  mutationGql,
+  queryGql,
+  queryResponse,
+  subscriptionGql,
+} from '../test-utils';
 import { ClientInstance } from '../types';
+import { CombinedError } from './';
+import { createClient } from './client';
 
 const url = 'https://hostname.com';
 const fetch = (global as any).fetch as jest.Mock;
+const onSubscriptionUpdate = jest.fn();
 
 const wait = (delay: number = 90) =>
   new Promise(resolve => setTimeout(resolve, delay));
@@ -61,7 +68,7 @@ describe('createClient', () => {
         const client = createClient({
           url,
           fetchOptions: fetchOptions as any,
-        }).createInstance({ onChange });
+        }).createInstance({ onChange, onSubscriptionUpdate });
 
         client.executeQuery(queryGql);
 
@@ -84,7 +91,7 @@ describe('createClient', () => {
       it('is passed to fetch', async () => {
         const client = createClient({
           url,
-        }).createInstance({ onChange });
+        }).createInstance({ onChange, onSubscriptionUpdate });
 
         client.executeQuery(queryGql);
 
@@ -103,7 +110,7 @@ describe('createInstance', () => {
   });
 
   it('passes snapshot', () => {
-    const instance = client.createInstance({ onChange });
+    const instance = client.createInstance({ onChange, onSubscriptionUpdate });
     expect(instance).toMatchSnapshot();
   });
 });
@@ -115,7 +122,7 @@ describe('executeQuery', () => {
   beforeEach(() => {
     client = createClient({
       url,
-    }).createInstance({ onChange });
+    }).createInstance({ onChange, onSubscriptionUpdate });
     fetch.mockClear();
     onChange.mockClear();
     fetch.mockResolvedValue({
@@ -281,7 +288,7 @@ describe('execute mutation', () => {
   beforeEach(async () => {
     client = createClient({
       url,
-    }).createInstance({ onChange });
+    }).createInstance({ onChange, onSubscriptionUpdate });
 
     fetch.mockResolvedValue({
       status: 200,
@@ -311,3 +318,73 @@ describe('execute mutation', () => {
     });
   });
 });
+
+describe('execute subscription', () => {
+  const forwardSubscription = jest.fn((operation, observer) =>
+    observer.next({ data: { data: 'foo' }, operation, error: null })
+  );
+
+  const client = createClient({
+    url,
+    forwardSubscription,
+  }).createInstance({ onChange: jest.fn(), onSubscriptionUpdate });
+
+  beforeEach(() => {
+    forwardSubscription.mockClear();
+    onSubscriptionUpdate.mockClear();
+  });
+
+  it('calls through the subscription exchange', () => {
+    client.executeSubscription(subscriptionGql);
+
+    expect(forwardSubscription).toHaveBeenCalled();
+  });
+
+  it('transform gql errors into CombinedError', () => {
+    forwardSubscription.mockImplementationOnce((operation, observer) =>
+      observer.next({
+        data: { data: null, errors: [{ name: 'An Error' }] },
+        operation,
+      })
+    );
+
+    client.executeSubscription(subscriptionGql);
+
+    expect(onSubscriptionUpdate.mock.calls[0][0].error).toBeInstanceOf(
+      CombinedError
+    );
+  });
+
+  it('calls through to the onSubscriptionUpdate', () => {
+    client.executeSubscription(subscriptionGql);
+
+    expect(onSubscriptionUpdate).toHaveBeenCalledWith(
+      expect.objectContaining({ data: 'foo' })
+    );
+  });
+});
+
+describe('execute unsubscribe subscription', () => {
+  const unsubscribe = jest.fn();
+
+  const forwardSubscription = jest.fn(() => {
+    return { unsubscribe };
+  });
+
+  const client = createClient({
+    url,
+    forwardSubscription,
+  }).createInstance({ onChange: jest.fn(), onSubscriptionUpdate });
+
+  beforeEach(() => {
+    forwardSubscription.mockClear();
+    onSubscriptionUpdate.mockClear();
+    client.executeSubscription(subscriptionGql);
+  });
+
+  it('calls the unsubscribe fn from the exchange library', () => {
+    client.executeUnsubscribeSubscription(subscriptionGql);
+
+    expect(unsubscribe).toHaveBeenCalled();
+  });
+});
diff --git a/src/test-utils/samples.ts b/src/test-utils/samples.ts
index 719a6073..2ff5c5ad 100644
--- a/src/test-utils/samples.ts
+++ b/src/test-utils/samples.ts
@@ -1,4 +1,11 @@
-import { Query, Mutation, Operation, ExchangeResult } from '../types';
+import {
+  ExchangeResult,
+  Mutation,
+  Operation,
+  OperationType,
+  Query,
+  Subscription,
+} from '../types';
 
 const context = {
   fetchOptions: {
@@ -31,7 +38,7 @@ export const mutationGql: Mutation = {
   },
 };
 
-export const subscriptionGql: Query = {
+export const subscriptionGql: Subscription = {
   query: `subscription subscribeToUser($user: String){
     user(user: $user) {
       name
@@ -45,21 +52,21 @@ export const subscriptionGql: Query = {
 
 export const queryOperation: Operation = {
   key: '2',
-  operationName: 'query',
+  operationName: OperationType.Query,
   context,
   ...queryGql,
 };
 
 export const mutationOperation: Operation = {
   key: JSON.stringify(mutationGql),
-  operationName: 'mutation',
+  operationName: OperationType.Mutation,
   context,
   ...mutationGql,
 };
 
 export const subscriptionOperation: Operation = {
   key: JSON.stringify(subscriptionGql),
-  operationName: 'subscription',
+  operationName: OperationType.Subscription,
   context,
   ...subscriptionGql,
 };