Trusted Documents

GraphQL APIs provide clients with considerable flexibility to query any data they need. This flexibility represents one of GraphQL's major strengths, but it also introduces vulnerabilities. When any client can query any data, malicious or careless queries can create excessive load on the server. Trusted Documents solve this problem.

The concept has existed since the early days of GraphQL, using terms like persisted queries or persisted operations. An API that uses Trusted Documents accepts only GraphQL documents (queries, operations) submitted (trusted) at development or deployment time. Instead of sending the whole document, clients send a more compact document id. This approach enhances security by rejecting malicious queries and improves performance by transmitting only the document id, similar to what occurs in Automatic Persisted Queries.

Adopting Trusted Documents places constraints primarily on API clients. To enforce trusted documents in a Grafbase API, you simply set a single option in grafbase.toml (see below).

We will begin by exploring the more complex aspects of adopting Trusted Documents and then explain how to enforce them.

The purpose of Trusted Documents is to accept only queries on an allow-list. Start by generating and communicating that list. We call the allow-list document a manifest, and it takes the form of a JSON file. Your GraphQL client setup of choice creates the manifest.

The two most common setups for generating a trusted documents manifest are Relay Persisted Queries and Apollo Client operation manifests (JS, Kotlin, iOS). Grafbase natively supports both Relay and Apollo Client manifest formats. If you need support for another setup or manifest format, please contact us.

After you create a manifest JSON file that includes the GraphQL documents your application needs and the associated document IDs, submit the manifest using the grafbase CLI:

$ grafbase trust my-account/my-graph@main --manifest manifest.json --client-name ios-client

Let's break down the arguments:

  • grafbase trust my-account/graph-name@main: Like many other CLI commands, trust requires a graph reference in the format <account>/<graph>@<branch>. Remember that you must include the branch name to avoid defaulting to the production branch, which can introduce security risks.
  • --manifest manifest.json: Provide the file path to the JSON file generated by your client of choice.
  • --client-name: Each client of an API using trusted documents must identify itself with a client name using the x-grafbase-client-name HTTP header. Read on for more details.

After you submit the manifest, the API trusts the GraphQL documents in the manifest, associating them with their corresponding document IDs. The trust command applies to a single branch and a single client name. To enforce trusted documents across multiple branches or clients, you must trust the relevant documents for each combination.

In the previous section, we uploaded the trusted document manifests. Now, our API knows which documents to expect. Our GraphQL client needs to change its requests to the API in two ways:

  1. Send the x-grafbase-client-name header with the same name used when submitting the manifest with grafbase trust.
  2. Send the trusted document IDs instead of the document body in GraphQL requests.

For example in Relay:

function fetchQuery(operation, variables) { return fetch('/graphql', { method: 'POST', headers: { 'content-type': 'application/json', 'x-grafbase-client-name': 'ios-app', }, body: JSON.stringify({ // `doc_id` is also accepted. documentId: operation.id, // NOTE: pass md5 hash to the server // query: operation.text, // this is now obsolete because text is null variables, }), }).then(response => { return response.json() }) }

or with Apollo Client:

import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client' import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries' import { generatePersistedQueryIdsFromManifest } from '@apollo/persisted-query-lists' const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql', headers: { 'x-grafbase-client-name': 'ios-app', }, }) const persistedQueryLink = createPersistedQueryLink( generatePersistedQueryIdsFromManifest({ loadManifest: () => import('./path/to/persisted-query-manifest.json'), }), ) const client = new ApolloClient({ cache: new InMemoryCache(), link: persistedQueriesLink.concat(httpLink), })

On the server side, the process is straightforward. You will find one relevant section in grafbase.toml:

[trusted_documents] enabled = true # default: false bypass_header_name = "my-header-name" # default null bypass_header_value = "my-secret-value" # default null

Let’s review the options:

  • enabled: Set this to true to enable trusted documents. The gateway fetches the graph associated with a branch when it starts. The router recognizes Trusted Documents that you submit (with grafbase trust) for that branch only.
  • bypass_header_name and bypass_header_value: If both values are not null, the router accepts arbitrary GraphQL documents whenever you pass the configured HTTP header.