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:
- Go to your project settings.
- Find the "External Composition" section.
- Click the toggle button.
- Provide the URL of your HTTP endpoint and the secret.
- Click "Save"
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:
- Get the value of
X-Hive-Signature-256
header. - Take the raw body of the request.
- Use an HMAC hex digest (
sha256
) to compute the hash (body with the secret provided in the configuration). - 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
})