⚡️ Pro Feature ⚡️ This feature is bundled with GraphQL-Pro.

Pusher Implementation

GraphQL Pro includes a subscription system based on Redis and Pusher which works with any Ruby web framework.

After creating an app on Pusher and configuring the Ruby gem, you can hook it up to your GraphQL schema.

How it Works

This subscription implementation uses a hybrid approach:

So, the lifecycle goes like this:

Here’s another look:

1. Subscription is created in your app

          HTTP POST
        .---------->   write to Redis
      📱            ⚙️ -----> 💾
        <---------'
        X-Subscription-ID: 1234


2. Client opens a connection to Pusher

          websocket
      📱 <---------> ☁️


3. The app sends updates via Pusher

      ⚙️ ---------> ☁️ ------> 📱
        POST           update
      (via gem)   (via websocket)


4. When the client unsubscribes, Pusher notifies the app

          webhook
      ⚙️ <-------- ☁️  (disconnect) 📱

By using this configuration, you can use GraphQL subscriptions without hosting a push server yourself!

Database setup

Subscriptions require a persistent Redis database, configured with:

maxmemory-policy noeviction
# optional, more durable persistence:
appendonly yes

Otherwise, Redis will drop data that doesn’t fit in memory (read more in “Redis persistence”).

If you’re already using Redis in your application, see “Storing Data in Redis” for options to isolate data and tune your configuration.

Schema configuration

Add redis to your Gemfile:

gem 'redis'

and bundle install. Then create a Redis instance:

# for example, in an initializer:
$graphql_subscriptions_redis = Redis.new # default connection

Then, that Redis client is passed to the Subscription configuration:

class MySchema < GraphQL::Schema
  use GraphQL::Pro::Subscriptions, redis: $graphql_subscriptions_redis
end

That connection will be used for managing subscription state. All writes to Redis are prefixed with graphql:sub:.

Execution configuration

During execution, GraphQL will assign a subscription_id to the context hash. The client will use that ID to listen for updates, so you must return the subscription_id in the response headers.

Return result.context[:subscription_id] as the X-Subscription-ID header. For example:

result = MySchema.execute(...)
# For subscriptions, return the subscription_id as a header
if result.subscription?
  response.headers["X-Subscription-ID"] = result.context[:subscription_id]
end
render json: result

This way, the client can use that ID as a Pusher channel.

For CORS requests, you need a special header so that clients can read the custom header:

if result.subscription?
  response.headers["X-Subscription-ID"] = result.context[:subscription_id]
  # Required for CORS requests:
  response.headers["Access-Control-Expose-Headers"] = "X-Subscription-ID"
end

Read more here: “Using CORS”.

Webhook configuration

Your server needs to receive webhooks from Pusher when clients disconnect. This keeps your local subscription database in sync with Pusher.

In the Pusher web UI, Add a webhook for “Channel existence”

/subscriptions/pusher_webhook_configuration.png

Then, mount the Rack app for handling webhooks from Pusher. For example, on Rails:

# config/routes.rb

# Include GraphQL::Pro's routing extensions:
using GraphQL::Pro::Routes

Rails.application.routes.draw do
  # ...
  # Handle Pusher webhooks for subscriptions:
  mount MySchema.pusher_webhooks_client, at: "/pusher_webhooks"
end

This way, we’ll be kept up-to-date with Pusher’s unsubscribe events.

Authorization

To ensure the privacy of subscription updates, you should use a private channel for transport.

To use a private channel, add a channel_prefix: key to your query context:

MySchema.execute(
  query_string,
  context: {
    # If this query is a subscription, use this prefix for the Pusher channel:
    channel_prefix: "private-user-#{current_user.id}-",
    # ...
  },
  # ...
)

That prefix will be applied to GraphQL-related Pusher channel names. (The prefix should begin with private-, as required by Pusher.)

Then, in your auth endpoint, you can assert that the logged-in user matches the channel name:

if params[:channel_name].start_with?("private-user-#{current_user.id}-")
  # success, render the auth token
else
  # failure, render unauthorized
end

Serializing Context

Since subscription state is stored in the database, then reloaded for pushing updates, you have to serialize and reload your query context.

By default, this is done with GraphQL::Subscriptions::Serialize’s dump and load methods, but you can provide custom implementations as well. To customize the serialization logic, create a subclass of GraphQL::Pro::Subscriptions and override #dump_context(ctx) and #load_context(ctx_string):

class CustomSubscriptions < GraphQL::Pro::Subscriptions
  def dump_context(ctx)
    context_hash = ctx.to_h
    # somehow convert this hash to a string, return the string
  end

  def load_context(ctx_string)
    # Given the string from the DB, create a new hash
    # to use as `context:`
  end
end

Then, use your custom subscriptions class instead of the built-in one for your schema:

class MySchema < GraphQL::Schema
  # Use custom subscriptions instead of GraphQL::Pro::Subscriptions
  # to get custom serialization logic
  use CustomSubscriptions, redis: $redis
end

That gives you fine-grained control of context reloading.

Dashboard

You can monitor subscription state in the GraphQL-Pro Dashboard:

/subscriptions/redis_dashboard_1.png

/subscriptions/redis_dashboard_2.png

Development Tips

Clear subscription data

At any time, you can reset your subscription database with the “Reset” button in the GraphQL-Pro Dashboard, or in Ruby:

# Wipe all subscription data from the DB:
MySchema.subscriptions.clear

Developing with Pusher webhooks

To receive Pusher’s webhooks in development, Pusher suggests using ngrok. It gives you a public URL which you can setup with Pusher, then any hooks delivered to that URL will be forwarded to your development environment.

Client configuration

Install the Pusher JS client then see docs for Apollo Client%20or%20%20%5BRelay%20Modern%5D(/javascript_client/relay_subscriptions).