Features
External Schema Composition

External Schema Composition

External Schema Composition allows you to build and validate GraphQL Schema outside of GraphQL Hive. When enabled, GraphQL Hive will send necessary information over HTTP to your HTTP endpoint and expect a composition result in return.

You most likely don't need this feature, but in some rare cases, it can be useful.

The most common reasons for using the external schema composition are licensing and limited language support (GraphQL Hive runs on NodeJS).

Configuration

To enable external schema composition:

  1. Go to your project settings.
  2. Find the "External Composition" section.
  3. Click the toggle button.

Disabled external schema composition

  1. Provide the URL of your HTTP endpoint and the secret.
  2. Click "Save"

External schema composition form

HTTP Endpoint

Your HTTP endpoint must be a POST endpoint that accepts JSON and returns JSON.

Securing your endpoint

To make sure your server is only receiving requests from GraphQL Hive, you can use the secret provided in the configuration and verify the signature of the request.

Please contact us if you wish to limit access by an IP address.

The logic of verifying the signature is as follows:

  1. Get the value of X-Hive-Signature-256 header.
  2. Take the raw body of the request.
  3. Use an HMAC hex digest (sha256) to compute the hash (body with the secret provided in the configuration).
  4. Compare the result with the value of the X-Hive-Signature-256 header (use "constant-time" comparison).

A NodeJS example of the verification process:

import crypto from 'node:crypto'
 
const sigHashAlg = 'sha256'
 
function hash(secret: string, data: string) {
  return crypto.createHmac(sigHashAlg, secret).update(data, 'utf-8').digest('hex')
}
 
function verifyRequest(input) {
  const { body, signature, secret } = input
 
  if (!body) {
    return 'ERR_EMPTY_BODY'
  }
 
  const sig = Buffer.from(signature ?? '', 'utf8')
  const digest = Buffer.from(hash(secret, body), 'utf8')
 
  if (sig.length !== digest.length || !crypto.timingSafeEqual(digest, sig)) {
    return 'ERR_INVALID_SIGNATURE'
  }
 
  // signature is valid
}

Specification

Expected shape of data and samples.

Request

The request body will contain the following information:

type SchemaService = {
  sdl: String!
  name: String!
  url: String
}
 
type RequestBody = Array<SchemaService>

Example request:

[
  {
    "sdl": "type Query { users: [String] }",
    "name": "users",
    "url": "https://api.com/users"
  },
  {
    "sdl": "extend type Query { comments: [String] }",
    "name": "comments",
    "url": "https://api.com/comments"
  }
]
Response

The reponse payload should match the following type:

type CompositionResult = CompositionSuccess | CompositionFailure
 
type CompositionSuccess = {
  type: 'success'
  result: {
    supergraph: string
    sdl: string
  }
}
 
type CompositionFailure = {
  type: 'failure'
  result: {
    errors: Array<{
      message: string
      source: 'graphql' : 'composition'
    }>
  }
}

Example response:

{
  "type": "failure",
  "result": {
    "errors": [
      {
        "message": "Type \"Query\" was defined more than once.",
        "source": "graphql"
      }
    ]
  }
}

Apollo Federation v1 example

The following example shows how to implement an external composition endpoint for Apollo Federation v1 in NodeJS.

npm install @graphql-hive/external-composition
💡

The @graphql-hive/external-composition library provides a thin compose function to bring auto-completion and type-safety to your IDE.

import fastify from 'fastify'
import { parse, printSchema } from 'graphql'
import { composeAndValidate, compositionHasErrors } from '@apollo/federation'
import { compose, signatureHeaderName, verifyRequest } from '@graphql-hive/external-composition'
 
const composeFederation = compose(services => {
  const result = composeAndValidate(
    services.map(service => {
      return {
        typeDefs: parse(service.sdl),
        name: service.name,
        url: service.url
      }
    })
  )
 
  if (compositionHasErrors(result)) {
    return {
      type: 'failure',
      result: {
        errors: result.errors.map(error => ({
          message: error.message,
          source: typeof error.extensions?.code === 'string' ? 'composition' : 'graphql'
        }))
      }
    }
  } else {
    return {
      type: 'success',
      result: {
        supergraph: result.supergraphSdl,
        sdl: printSchema(result.schema)
      }
    }
  }
})
 
const server = fastify()
 
server.route({
  method: ['POST'],
  url: '/compose',
  handler(req, res) {
    const error = verifyRequest({
      // Stringified body, or raw body if you have access to it
      body: JSON.stringify(req.body),
      // Pass here the signature from `X-Hive-Signature-256` header
      signature: req.headers[signatureHeaderName],
      // Pass here the secret you configured in GraphQL Hive
      secret: YOUR_SECRET_HERE
    })
 
    if (error) {
      // Failed to verify the request - send 500 and the error message back
      res.status(500).send(error)
    } else {
      const result = composeFederation(req.body)
      // Send the result back (as JSON)
      res.send(JSON.stringify(result))
    }
  }
})
 
await server.listen({
  port: 3000
})

Apollo Federation v2 example

Pre-built Docker image

We provide a Docker image (opens in a new tab) for running external composition service for Apollo Federation v2.

The pre-built image implements the best-practice to secure your endpoint, and uses the latest version of Apollo Federation v2.

Start by deciding on your encryption secret. This is needed in order to ensure you endpoint is secured and can be triggered only by Hive platform. Your secret can be any string you decide, and it will be used as private key to hash the requests to your composition service.

To run the container, you can use the following command:

docker run -p 3069:3069 -e SECRET="MY_SECRET_HERE" ghcr.io/kamilkisiela/graphql-hive/composition-federation-2

You should make this service publicly available, and then configure it in Hive platform (see Configuration section above).

The container created here listens to POST /compose requests, so the endpoint you are using should be composed from the public endpoint of this service, and the /compose path.

Custom NodeJS Server

You can also build your own server for the composition endpoint. The following example shows how to do that.

The following example shows how to implement an external composition endpoint for Apollo Federation v2 in NodeJS.

npm install @graphql-hive/external-composition
💡

The @graphql-hive/external-composition library provides a thin compose function to bring auto-completion and type-safety to your IDE.

import fastify from 'fastify'
import { parse, printSchema } from 'graphql'
import { composeServices } from '@apollo/composition'
import { compose, signatureHeaderName, verifyRequest } from '@graphql-hive/external-composition'
 
const composeFederation = compose(services => {
  const result = composeServices(
    services.map(service => {
      return {
        typeDefs: parse(service.sdl),
        name: service.name,
        url: service.url
      }
    })
  )
 
  if (result.errors?.length) {
    return {
      type: 'failure',
      result: {
        errors: result.errors.map(error => ({
          message: error.message,
          source: typeof error.extensions?.code === 'string' ? 'composition' : 'graphql'
        }))
      }
    }
  } else {
    return {
      type: 'success',
      result: {
        supergraph: result.supergraphSdl,
        sdl: printSchema(result.schema.toGraphQLJSSchema())
      }
    }
  }
})
 
const server = fastify()
 
server.route({
  method: ['POST'],
  url: '/compose',
  handler(req, res) {
    const error = verifyRequest({
      // Stringified body, or raw body if you have access to it
      body: JSON.stringify(req.body),
      // Pass here the signature from `X-Hive-Signature-256` header
      signature: req.headers[signatureHeaderName],
      // Pass here the secret you configured in GraphQL Hive
      secret: YOUR_SECRET_HERE
    })
 
    if (error) {
      // Failed to verify the request - send 500 and the error message back
      res.status(500).send(error)
    } else {
      const result = composeFederation(req.body)
      // Send the result back (as JSON)
      res.send(JSON.stringify(result))
    }
  }
})
 
await server.listen({
  port: 3000
})