Trusted Documents
GraphQL APIs give their clients considerable flexibility to query whatever data they need. This is one of the major strengths of GraphQL, but also a source of vulnerability. When any client can query any data, malicious or careless queries can put excessive load on the server. Trusted Documents are a solution to this problem.
The general concept has been around since the early days of GraphQL under different names, most commonly persisted queries or persisted operations. In a nutshell, an API using Trusted Documents will only accept GraphQL documents (queries, operations) that have been submitted (trusted) at development or deployment time. Instead of the whole document, clients send a more compact document id. Security benefits from malicious queries being rejected, and performance is improved by sending only the document id, just like in Automatic Persisted Queries.
Trusted Documents is available on the Enterprise plan. Contact us to learn more.
Adopting Trusted Documents imposes constraints mainly for clients of the API. In a Grafbase API, enforcing trusted documents is as simple as a single setting in grafbase.toml
(see below).
We'll start with the more involved aspects of adopting Trusted Documents, then see how to enforce them.
Since the point of Trusted Documents is to only accept queries on an allow-list, we will start by generating and communicating that list. The allow-list document is generally called a manifest and takes the form of a JSON file. The responsibility for creating the manifest lies with your GraphQL client setup of choice.
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 are interested in support for another setup or manifest format, please contact us.
Once you have a manifest JSON file that contains the GraphQL documents your application needs to send and the associated document IDs, you are ready to submit the manifest with 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
takes a graph reference of the form<account>/<graph>@<branch>
. Please note that the branch name is mandatory here, because defaulting to the production branch would be potentially insecure.--manifest manifest.json
: the file path to the JSON file generated by your client of choice.--client-name
: every client of an API using trusted documents is required to identify itself with a client name, using thex-grafbase-client-name
HTTP header. Continue reading for more details.
Now that the manifest has been submitted, the GraphQL documents in the manifest are trusted and associated with the corresponding document IDs. The trust command is scoped to a single branch and a single client name. If you want to enforce trusted documents on multiple branches, or with multiple clients, you will need to trust the relevant documents for every combination.
We uploaded the trusted document manifests in the previous section, so that our API knows which documents to expect. Now our GraphQL client needs to change its requests to the API in two ways:
- Send the
x-grafbase-client-name
header with the same name we used when submitting the manifest withgrafbase trust
. - 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 story is much simpler. There is 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 us go through the options:
enabled
: set this totrue
to enable trusted documents. The graph fetched by the gateway when it starts is associated with a branch. The router will recognize Trusted Documents submitted (withgrafbase trust
) for that branch only.bypass_header_name
andbypass_header_value
: if both are not null, the router will accept arbitrary GraphQL documents whenever the HTTP header configured here is also passed.
If you do not use the self-hosted Grafbase gateway, you can still use Trusted Documents. You can enable and configure them in your grafbase.config.ts
. For example:
import { config, connector, graph } from '@grafbase/sdk'
const g = graph.Standalone()
const vendure = connector.GraphQL('Vendure', {
url: env.VENDURE_GRAPHQL_API_UR,
})
g.datasource(vendure, { namespace: false })
export default config({
graph: g,
trustedDocuments: {
enabled: true,
bypassHeader: {
name: 'x-secret-header',
value: 'secret-value',
},
},
})