• v3 (latest)
  • Features
  • Persisted Operations

Persisted Operations

Persisted operations is a mechanism for preventing the execution of arbitrary GraphQL operation documents. By default, the persisted operations plugin follows the the APQ Specification for SENDING hashes to the server.

However, you can customize the protocol to comply to other implementations e.g. used by Relay persisted queries.

change this behavior by overriding the getPersistedOperationKey option to support Relay's specification for example.

Quick Start

Persisted operations requires installing a separate package.

yarn add @graphql-yoga/persisted-operations
Persisted operation setup
import { createYoga, createSchema } from 'graphql-yoga'
import { createServer } from 'node:http'
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'
 
const store = {
  ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38:
    '{__typename}'
}
 
const yoga = createYoga({
  schema: createSchema({
    typeDefs: /* GraphQL */ `
      type Query {
        hello: String!
      }
    `
  }),
  plugins: [
    usePersistedOperations({
      getPersistedOperation(sha256Hash: string) {
        return store[sha256Hash]
      }
    })
  ]
})
 
const server = createServer(yoga)
server.listen(4000, () => {
  console.info('Server is running on http://localhost:4000/graphql')
})

Start your yoga server and send the following request.

Execute persisted GraphQL operation
curl -X POST -H 'Content-Type: application/json' http://localhost:4000/graphql \
  -d '{"extensions":{"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}}'
 
{"data":{"__typename":"Query"}}

As you can see, the persisted operations plugin is able to execute the operation without the need to send the full operation document.

If you now sent a normal GraphQL operation that is not within the store, it will be rejected.

Arbitary GraphQL operation
curl -X POST -H 'Content-Type: application/json' http://localhost:4000/graphql \
  -d '{"query": "{__typename}"}'
 
{"errors":[{"message":"PersistedQueryOnly"}]}

Extracting client operations

You can use GraphQL Code Generator with the graphql-codegen-persisted-query-ids plugin for extracting a map of persisted query ids and their corresponding GraphQL documents from your application/client-code in a JSON file.

Example map extracted by GraphQL Code Generator
{
  "ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38": "{__typename}",
  "c7a30a69b731d1af42a4ba02f2fa7a5771b6c44dcafb7c3e5fa4232c012bf5e7": "mutation {__typename}"
}

This map can then be used to persist the GraphQL documents in the server.

Use JSON file from filesystem as the persisted operation store
import { createYoga, createSchema } from 'graphql-yoga'
import { createServer } from 'node:http'
import { readFileSync } from 'node:fs'
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'
 
const persistedOperations = JSON.parse(
  readFileSync('./persistedOperations.json', 'utf-8')
)
 
const yoga = createYoga({
  schema: createSchema({
    typeDefs: /* GraphQL */ `
      type Query {
        hello: String!
      }
    `
  }),
  plugins: [
    usePersistedOperations({
      getPersistedOperation(key: string) {
        return persistedOperations[key]
      }
    })
  ]
})
 
const server = createServer(yoga)
server.listen(4000, () => {
  console.info('Server is running on http://localhost:4000/graphql')
})

Sending the hash from the client

The persisted operations plugin follows the the APQ Specification of Apollo for SENDING hashes to the server.

GraphQL clients such Apollo Client and Urql support that out of the box. Check the corresponding documentation for more information.

Allowing arbitrary GraphQL operations

Sometimes it is handy to allow non-persisted operations aside from the persisted ones. E.g. you want to allow developers to execute arbitrary GraphQL operations on your production server.

This can be achieved using the allowArbitraryOperations option.

Allow arbitrary GraphQL operations
const plugin = usePersistedOperations({
  allowArbitraryOperations: (request) =>
    request.headers.request.headers.get('x-allow-arbitrary-operations') ===
    'true'
})

Use this option with caution!

Using Relay's Persisted Queries Specification

If you are using Relay's Persisted Queries specification, you can configure the plugin like below;

Relay Persisted Queries example
import { createYoga, createSchema } from 'graphql-yoga'
import { createServer } from 'node:http'
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'
 
const store = {
  ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38:
    '{__typename}'
}
 
const yoga = createYoga({
    schema: createSchema({
    typeDefs: /* GraphQL */ `
      type Query {
        hello: String!
      }
    `
  }),
  plugins: [
    usePersistedOperations({
      extractPersistedOperationId(params: Record<string, unknown>) {
        if (typeof payload.doc_id === 'string') {
          return payload.doc_id
        }
        return null
      }
      getPersistedOperation(key: string) {
        return store[key]
      },
    }),
  ],
})
 
const server = createServer(yoga)
server.listen(4000, () => {
    console.info('Server is running on http://localhost:4000/graphql')
})

Using an external Persisted Operation Store

As a project grows the amount of GraphQL Clients and GraphQL Operations can grow a lot. At some point it might become impractible to store all persisted operations in memory.

In such a scenario you can use an external persisted operation store.

You can return a Promise from the getPersistedOperation function and call any database or external service to retrieve the persisted operation.

💡

For the best performance a mixture of an LRU in-memory store and external persisted operation store is recommended.

Use external persisted operation store
import { createYoga } from 'graphql-yoga'
import { createServer } from 'node:http'
import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'
 
const yoga = createYoga({
  plugins: [
    usePersistedOperations({
      async getPersistedOperation(key: string) {
        return await fetch(`https://localhost:9999/document/${key}`).then(
          (res) => res.json()
        )
      }
    })
  ]
})
 
const server = createServer(yoga)
server.listen(4000, () => {
  console.info('Server is running on http://localhost:4000/graphql')
})
Last updated on November 15, 2022