Updated on 25 December 2019 .

Here are some notes on WebSockets-based Absinthe GraphQL subscriptions. A basic setup and familiarity with regular queries and mutations is assumed.

Before we write our first subscription we need to ensure that the Absinthe.Subscription process will be started:

# lib/app/application.ex ... # Start the endpoint when the application starts AppWeb . Endpoint , # GraphQL subscriptions { Absinthe . Subscription , [ AppWeb . Endpoint ]}, ...

Next we have to update our Phoenix application endpoint to have the new WebSocket endpoint in place. Most people would want to start with one endpoint, use WebSockets, and handle the connection per user, hence pointing to AppWeb.UserSocket module for authentication.

# lib/app_web/endpoint.ex defmodule AppWeb . Endpoint do use Phoenix . Endpoint , otp_app: :app use Absinthe . Phoenix . Endpoint socket "/socket" , AppWeb . UserSocket , websocket: true , longpoll: false ...

AppWeb.UserSocket module needs to be defined of course. Here is an example of authenticating this socket connection using Guardian and OAuth 2 bearer token. We either return {:ok, socket} tuple for successful connection or :error atom to refuse the request. Also notice the Absinthe.Phoenix.Socket.put_options function call to add the user identifier (we will use this soon in the subscription):

# lib/app_web/channels/user_socket.ex defmodule AppWeb . UserSocket do use Phoenix . Socket use Absinthe . Phoenix . Socket , schema: DigiWeb . Private . Schema def connect (%{ "Authorization" => header_content }, socket ) do [[ _ , token ]] = Regex . scan ( ~r/^Bearer (.*)/ , header_content ) case Guardian . Phoenix . Socket . authenticate ( socket , DigiWeb . Public . Guardian , token ) do { :ok , authed_socket } -> user_id = authed_socket . assigns . guardian_default_claims [ "sub" ] new_socket = Absinthe . Phoenix . Socket . put_options ( authed_socket , context: %{ user_id: user_id } ) { :ok , new_socket } { :error , _ } -> :error end end # This function will be called when there was no authentication information def connect ( _params , _socket ) do :error end def id ( _socket ), do : nil end

Once the connection itself is ready, we can update our GraphQL schema with a new subscription. I am including two similar yet different examples of a subscription for a file download. :user_download_ready uses the socket context to publish to a “user_downloads:USER_ID” topic and :download_ready uses a :download_id identifier instead to publish to a “downloads:DOWNLOAD_ID” topic (this would require the download request mutation to return such ID):

defmodule MyWeb . GraphQL . Schema do ... subscription do field :user_download_ready , :download_link do config ( fn args , context -> { :ok , topic: "user_downloads: #{ context [ :context ][ :user_id ] } " } end ) resolve ( fn link , _ , _res -> { :ok , link } end ) end field :download_ready , :download_link do # Unique download ID of the job arg ( :download_id , non_null ( :string )) config ( fn args , _context -> { :ok , topic: "downloads: #{ args . download_id } " } end ) resolve ( fn link , _ , _res -> { :ok , link } end ) end end end

If we would want to test this setup without initiating a download request (like sending a mutation) we can use Absinthe.Subscription.publish to publish new link that will be then resolve with the above resolver function. We can then watch console output on what’s happening.

Absinthe . Subscription . publish ( AppWeb . Endpoint , %{ url: "testurl" }, user_download_ready: "user_downloads:1" )

If we want to test our new subscription using GraphiQL we need to update our router to point GraphiQL to the right socket module:

# lib/app_web/router.ex ... scope "/api/graphiql" do pipe_through [ :browser , :authenticate_on_post_only ] forward "/" , Absinthe . Plug . GraphiQL , schema: AppWeb . GraphQL . Schema , socket: AppWeb . UserSocket ...

And then make sure that we are setting the correct WS URL for the WebSocket connection next to the usual URL in the top part of the GraphiQL. In case of using SSL/TLS this would be:

URL: https://localhost:4443/graphql

WS URL: wss://localhost:4443/socket

At last we just send the subscription request and wait for the results to appear as they come:

subscription { userDownloadReady () { url } }

Note that in our case the user is determined by the OAuth 2 bearer token in the Authorization header in the format of Bearer TOKEN that we added in the top part of the GraphiQL under the API and WS URLs.

Published on 25 December 2019 . Tags: api elixir graphql phoenix

Any comments? Write me a DM on Twitter.