• 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
    }>
  }
}

Example response:

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

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 { verifyRequest, compose, signatureHeaderName } from '@graphql-hive/external-composition'
import { composeAndValidate, compositionHasErrors } from '@apollo/federation'
import { parse, printSchema } from 'graphql'
import fastify from 'fastify'
 
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(err => ({
          message: err.message
        }))
      }
    }
  } 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

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 { verifyRequest, compose, signatureHeaderName } from '@graphql-hive/external-composition'
import { composeServices } from '@apollo/composition'
import { parse, printSchema } from 'graphql'
import fastify from 'fastify'
 
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
        }))
      }
    }
  } 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
})