Join us for GraphQL Summit, October 10-12 in San Diego. Use promo code ODYSSEY for $400 off your pass.
Launch GraphOS Studio

Safelisting with persisted queries

Secure your graph while minimizing request latency

This feature is available only with a GraphOS Enterprise plan. If your organization doesn't currently have an Enterprise plan, you can test this functionality by signing up for a free Enterprise trial.

This feature is currently in preview. Your questions and feedback are highly valueddon't hesitate to get in touch with your Apollo contact or on the official Apollo GraphQL Discord.

GraphQL APIs are broadly open by design. They let client applications send queries with arbitrary shapes and sizes. And while this allows for expedited client development and a highly performant API platform, it necessitates securing your graph against potentially malicious requests.

With GraphOS Enterprise, you can enhance your 's security by maintaining a persisted query list (PQL) for your 's self-hosted . The Apollo Router checks incoming requests against the PQL, an safelist made by your first-party apps.

Apollo Router
Query List
First-party apps
Web client
Android client
iOS client

Your can use its persisted query list (PQL) to both protect your supergraph and speed up your clients' operations:

  • When you enable safelisting, your rejects any incoming s not registered in its PQL.
  • Client apps can execute an by providing its PQL-specified ID instead of an entire string.
    • Querying by ID can significantly reduce latency and bandwidth usage for very large strings.
    • Your can require that clients provide s by ID and reject full strings—even operation strings present in the PQL.

Differences from automatic persisted queries

The Apollo also supports a related feature called automatic persisted queries (APQ). With APQ, clients can execute a GraphQL by sending the SHA256 hash of its operation string instead of the full string.

APQ has a few limitations compared to preregistered persisted queries.

automatic persisted queries Preregistered persisted queries
Query performance✅ Clients can send identifiers instead of full query strings, reducing request sizes dramatically and latency.✅ Same as APQ
Build- vs. runtime registration Queries are registered at runtime. One of your router instances must receive any given operation string from a client at least once to cache it. Clients contribute to the PQL at build-time. Your router fetches its PQL from GraphOS on startup and polls for updates, meaning clients can always execute operations using their PQL-specified ID.
Safelisting❌ APQ doesn't provide safelisting capabilities because the router dynamically populates its APQ cache over time with any operations it receives.✅ Clients preregister their operations to GraphOS. Your router fetches its PQL on startup, enabling it to reject operations not present in the PQL.

If you only want to improve request latency and bandwidth usage, APQ addresses your use case. If you also want to secure your with safelisting, you should preregister operations in a PQL.

Security levels

The Apollo supports the following security levels or modes, in increasing order of restrictiveness:

Security LevelDescription
Allow operation IDsClients can optionally execute an operation on your router by providing the operation's PQL-specified ID.
Audit modeExecuting operations by providing a PQL-specified ID is still optional, but the router logs any unregistered operations.
SafelistingThe router rejects any incoming operations that aren't present in its PQL. Clients can use either PQL-specified ID or operation string to execute operations.
Safelisting with IDs onlyClients can _only_ execute operations by providing their PQL-specified IDs; the router rejects all freeform GraphQL requests.

You can find more details, including configuration instructions, in the implementation section.

These levels of permissiveness allow you to incrementally adopt persisted queries on a client-by-client basis. Specifically, the should use audit mode until you're confident that all your clients' trusted s have been preregistered in the PQL. Refer to the incremental adoption section for a step-by-step guide.

Implementation steps

Persisted queries provide benefits to different teams:

  • Safelisting helps platform teams secure the graph and optimize its performance.
  • Application developers can use preregistered IDs to write performant client code.

Implementation also requires collaboration among these parties. These are the main steps for implementing persisted queries for safelisting, along with the team that usually performs them:

StepDescriptionResponsible party
1. PQL creation and linking Create and apply a PQL to graph variants. Platform team
2. Router configuration Update your router's YAML config file to enable persisted queries at the appropriate security level.Platform team
3. Preregister operations Generate and publish a persisted queries manifest (PQM) to the PQL from your client's CI/CD pipeline.App developers
4. Client updates (Optional) Update clients to use operation IDs rather than full query strings.

This step provides performance benefits but isn't necessary for safelisting.
App developers

Continue reading for each step's details, or skip to the incremental adoption section for the recommended incremental adoption strategy. (This section assumes you have a high-level understanding of each implementation step.)

1. PQL creation and linking

To use persisted queries, you first need a persisted query list (PQL) in Studio. Platform teams create an empty PQL in GraphOS Studio so that client teams can preregister s to it.

Each PQL is associated or "linked" with a single graph in . A graph, however, can have several PQLs. For example, one graph may need multiple PQLs if you want a separate PQL for each contract variant. You can link a PQL to any s of its graph. And although many variants may use the same PQL, each variant can only have one linked PQL at a time.

1.1 PQL creation

  1. From your organization's Graphs page in GraphOS Studio, open the PQL page for a graph by clicking its PQL button:

    The persisted query list button in the Studio graph list

    You can also access a graph's PQLs from its settings page.

  2. From the PQL page:

    • If you haven't created any PQLs yet, click Create a Persisted Query List.
    • If you already have at least one PQL, click New List in the upper right.

    The following dialog appears:

    The first step of the PQL creation dialog in GraphOS studio
  3. Provide a name and (optional) description for your PQL, then click Create.

    • At this point, your empty PQL has been created. The remaining dialog steps help with additional setup.
  4. The second dialog step (Link) enables you to link your new PQL to one existing of your graph.

    • You can optionally Skip this step and link s later (covered in the next step).
  5. The third dialog step (Publish) displays your new PQL's unique ID and an example CLI command for publishing s to the PQL.

    • For now, you can leave the PQL empty. Client teams can publish s to it in a later step.
    • Save this CLI command so you can pass it on to your client teams when they publish s.
  6. The fourth and final dialog step (Configure) displays the configuration options you apply to your to begin using your PQL. We'll cover these in a later step.

  7. Click Finish to close the dialog. Your newly created PQL appears in the table:

    A newly created PQL in the table on the Studio PQL page

After you create a PQL, you can link it to one or more s of your graph. Each instance associated with a linked variant automatically fetches its PQL from .

It's safe to link an empty or incomplete PQL to a because your doesn't use its PQL for anything until you configure it to do so (covered in a later step).

  1. From the table on your graph's PQL page, open the ••• menu under the Actions column for the PQL you want to link:

    The Actions menu for a PQL in Studio
  2. Click Link and Unlink Variants. The following dialog appears:

    Dialog for linking PQLs to variants in Studio
  3. Use the dropdown menu to select any s you want to link your PQL to.

As a best practice, you can begin by linking your PQLs to a staging environment before moving on to a production one.

  1. Click Save.

After you link a PQL to a :

  1. validates the PQL against the 's reported history and flags any recent operations not represented in the PQL.

  2. then uploads the PQL to Uplink, the service that delivers configuration to your at runtime.

2. Router configuration

The Apollo is the key component that enforces safelisting.

As soon as a graph has an associated PQL, you can configure instances to fetch and use the PQL by following these steps:

  1. Ensure your instances are ready to work with PQLs:

    • Make sure you're using version 1.25.0 or later of the Apollo .
    • Make sure your instances are connected to your GraphOS Enterprise organization and that they're associated with a that your PQL is linked to.
  2. Set your desired security level in your 's YAML config file. For supported options, see router security levels. When first implementing persisted queries, it's best to start with audit—or "dry run"—mode.

  3. Deploy your updated instances to begin using your PQL.

Once your organization's PQL has preregistered all your clients' s and you've ensured your client apps are only sending preregistered operations, you can update your configuration to the safelisting security level.

Router security levels

The Apollo supports the following security levels, in increasing order of restrictiveness:

  • Allow operation IDs: Clients can optionally execute an on your by providing the operation's PQL-specified ID.
    • All other levels also provide this core capability.
    • This level doesn't provide safelisting.
  • Audit mode: Executing s by providing a PQL-specified ID is still optional, but the also logs any unregistered operations.
    • The level serves as a dry run and helps you identify s you may still need to preregister before turning on safelisting.
  • Safelisting: The rejects any incoming s not present in its PQL. Requests can use either ID or operation string.
    • Before moving to this security level, ensure all your client s are present in your PQL.
  • Safelisting with IDs only: The rejects any freeform GraphQL s. Clients can only execute s by providing their PQL-specified IDs.
    • Before moving to this security level, ensure all your clients execute s by providing their PQL-specified ID.

When adopting persisted queries, you should start with a less restrictive security such as audit mode. You can then enable increasingly restrictive levels after your teams have updated all clients.

See below for sample YAML configurations for each level. Refer to the router configuration options for option details.

Allow operation IDs

To use persisted queries only to reduce network bandwidth and latency (not for safelisting), add the following minimal configuration:

enabled: true

Note: You can use this security level with or without automatic persisted queries enabled.

This mode lets clients execute s by providing their PQL-specified ID instead of the full operation string. Your also continues to accept full operation strings, even for operations that don't appear in its PQL.

Audit mode (dry run)

Turning on logging is crucial for gauging your client apps' readiness for safelisting. The logs identify which s you need to either add to your PQL or stop your client apps from making.

To enable logging for unregistered queries, enable the log_unknown property:

enabled: true
log_unknown: true

Note: You can use audit mode with or without automatic persisted queries enabled.

Unregistered s appear in your router's logs.

For example:

2023-08-02T11:51:59.833534Z WARN [trace_id=5006cef73e985810eb086e5900945807] unknown operation operation_body="query ExampleQuery {\n me {\n id\n }\n}\n"

If your receives an preregistered in the PQL, no log message will be output.

You can use these logs to audit s sent to your router and ask client teams to add new ones to your PQL if necessary.


⚠️ Before applying this configuration, ensure your PQL contains all GraphQL s that all active versions of your clients execute. If you enable safelisting without ensuring this, your will reject any unpublished client s.

With the following configuration, your allows only GraphQL s that are present in its PQL while rejecting all other operations:

enabled: true
log_unknown: true
enabled: true
require_id: false
enabled: false # APQ must be turned off

Note: To enable safelisting, you must turn off automatic persisted queries (APQs). APQs let clients register arbitrary operations at runtime while safelisting restricts s to those that have been explicitly preregistered.

To execute an , clients can provide its PQL-specified ID or full string. The rejects unregistered operations, and if log_unknown is true, those s appear in your router's logs.

So you can monitor the s your rejects, it's best to keep log_unknown as true while adopting safelisting. Once you're confident that all your clients are properly configured, you can turn it off to reduce noise in your logs.

Safelisting with IDs only

⚠️ Do not start with this configuration: It requires all your clients to execute s by providing their PQL-specified ID. If any clients still provide full operation strings, the rejects those operations, even if they're included in the safelist.

With the following configuration, your rejects all strings and only accepts preregistered operation IDs:

enabled: true
log_unknown: true
enabled: true
require_id: true
enabled: false # APQ must be turned off

Note: To enable safelisting, you must turn off automatic persisted queries (APQs). APQs let clients register arbitrary operations at runtime while safelisting restricts s to those that have been explicitly preregistered.

If you want to use this security level, you should always first set up safelisting with operation strings allowed. ID-only safelisting requires all your clients to execute s via PQL-specified ID instead of an operation string. While making those necessary changes, you can use the less restrictive safelisting mode in your .

With log_unknown set to true, the logs all rejected s, including those preregistered to your PQL but that used the full operation string rather than the PQL-specified ID.

So you can monitor the s your rejects, it's best to keep log_unknown as true while adopting safelisting. Once you're confident that all your clients are properly configured, you can turn it off to reduce noise in your logs.

3. Preregister operations

Preregistering s to a PQL has two steps:

  1. Generating persisted queries manifests (PQM) using client-specific tooling
  2. Publishing PQMs to the PQL using the CLI tool

Building both of these into your CI/CD pipeline incorporates new s automatically whenever you release a new client app version.

3.1 Generate persisted queries manifests

Once a PQL exists in , client teams can start publishing s to it. To do so, you must generate JSON manifests of the s to publish. You generate a separate manifest for each of your client apps.

You perform manifest generation in your CI/CD pipeline. Doing so automatically incorporates new s when you release a new client app version.

Generation methods

Apollo Client for Web, Kotlin, and iOS each provide a mechanism for generating a manifest file from your app source. Apollo also supports manifests generated by the Relay compiler.

If your client app uses another GraphQL client library, you can build your own mechanism for generating manifests. See the expected manifest format.

See the instructions for your client library:

Apollo Client Web

  1. In your app's project, install the @apollo/generate-persisted-query-manifest package as a dev dependency:

    npm install --save-dev @apollo/generate-persisted-query-manifest

    This package includes a CLI command to generate a manifest file from your application source.

  2. Generate your first manifest with the following command:

    npx generate-persisted-query-manifest
    • If the command succeeds, your manifest is written to persisted-query-manifest.json.
    • If the command fails (or if your manifest doesn't include all the s you expect it to), you can configure the command's behavior using the options described in the package README.

See the full Apollo Client persisted queries guide for detailed instructions.

Apollo Kotlin

Manifest generation requires Apollo Kotlin 3.8.2 or later.

To generate an manifest with Apollo Kotlin, you modify your project's Gradle plugin configuration to generate a manifest in addition to the standard Kotlin source for your s:

apollo {
service("myapi") {

The manifest will be generated in build/generated/manifest/apollo/myapi/persistedQueryManifest.json

See the full Apollo Kotlin persisted queries guide for detailed instructions.

Apollo iOS

Manifest generation requires Apollo iOS 1.4.0 or later.

To generate an manifest with Apollo iOS, you use the same code generation engine that you use to generate Swift code for each of your s. Specifically, you modify the engine's file output configuration to include the output of an operationManifest.

See the full Apollo iOS persisted queries guide for detailed instructions.

Relay compiler

The CLI has a built-in capability to publish manifests generated by the Relay compiler. Refer to Relay's documentation for instructions on generating manifests.

3.2 Publish manifests to the PQL

Ensure your CLI version is 0.17.2 or later. Previous versions of don't support publishing s to a PQL. Download the latest version.

After you generate an operation manifest, you publish it to your PQL with the Rover CLI like so:

Example command
rover persisted-queries publish my-graph@my-variant \
--manifest ./persisted-query-manifest.json
  • The my-graph@my-variant is the graph ref of any the PQL is linked to.
    • Graph refs have the format graph-id@variant-name.
  • Use the --manifest option to provide the path to the manifest you want to publish.

The persisted-queries publish command assumes manifests are in the format generated by Apollo client tools. The command can also support manifests generated by the Relay compiler by adding the --manifest-format relay . Your CLI version must be 0.19.0 or later to use this argument.

The persisted-queries publish command does the following:

  1. Publishes all s in the provided manifest file to the PQL linked to the specified , or to the specified PQL.

    • Publishing a manifest to a PQL is additive. Any existing entries in the PQL remain.
    • If you publish an with the same id but different details from an existing entry in the PQL, the entire publish command fails with an error.
  2. Updates any other s that the PQL is applied to so that s associated with those variants can fetch their updated PQL.

As with generating manifests, it's best to execute this command in your CI/CD pipeline to publish new s as part of your app release process. The API key you supply to must have the role of Graph Admin or Persisted Query Publisher. Persisted Query Publisher is a special role designed for use with the rover persisted-queries publish command; API keys with this role have no other access to your graph's data in , and are appropriate for sharing with trusted third party client developers who should be allowed to publish s to your graph's PQL but should not otherwise have access to your graph.

Test operations

You can send some test s to test that you've successfully published your manifests:

First, start your -connected :

APOLLO_KEY="..." APOLLO_GRAPH_REF="..." ./router --config ./router.yaml
2023-05-11T15:32:30.684460Z INFO Apollo Router v1.18.1 // (c) Apollo Graph, Inc. // Licensed as ELv2 (
2023-05-11T15:32:30.684480Z INFO Anonymous usage data is gathered to inform Apollo product development. See for details.
2023-05-11T15:32:31.507085Z INFO Health check endpoint exposed at
2023-05-11T15:32:31.507823Z INFO GraphQL endpoint exposed at 🚀

Next, make a POST request with curl, like so:

curl http://localhost:4000 -X POST --json \

If your 's PQL includes an with an ID that matches the value of the provided sha256Hash property, it executes the corresponding and returns its result.

4. Client updates

With your manifest published and the router configured, you can update your clients to use preregistered s IDs. Organizations can do this one client at a time as client teams publish client-specific PQMs to the PQL.

Note: This step provides performance benefits but isn't necessary for safelisting. You can continue to use full strings rather than operation IDs in safelisting mode.

To execute s using their PQL-specified ID instead of full operations strings, clients can use the same protocol used for automatic persisted queries (APQ).

Here's the JSON body of a request to execute an by its ID:

"variables": null,
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "PQL_ID_HERE"

If executing an that includes GraphQL s, specify them with the variables property.

Apollo's mobile clients let you use the same mechanism for executing persisted queries s as APQs. Refer to their persisted query documentation for implementation details.

With Apollo Client Web, sending persisted queries by ID requires you to use an additional package at runtime alongside @apollo/client's built-in createPersistedQueryLink. Apollo Client Web requires this package to ensure that the ID sent at runtime matches the ID generated by generate-persisted-query-manifest. Mobile clients have a more deterministic approach to formatting s and, thus, don't need additional support.

Refer to the Apollo Client Web's persisted query documentation for implementation details.

Incremental adoption path

Persisted queries' tiered security levels let you adopt an incremental approach rather than simultaneously requiring all clients to send requests via preregistered s IDs. You can follow these steps for incremental adoption:

  1. Identify the first client you want to implement persisted queries with. It could be the client or team you're most comfortable with or the one most comfortable with .

  2. Follow all implementation steps for your chosen client:

  3. Continue to monitor your router logs: once you consistently see that unregistered operations are being logged and preregistered ones aren't, you've completed the setup for this client! 🎉

If safelisting is your goal, you'll need to coordinate across client teams to complete these steps for each of your client apps.

Once your 's logs are completely clear of unexpected s, you can configure your router to use safelisting mode. Then, to reap the performance benefits, update your client apps to use IDs rather than full query strings.

Once you've confirmed all client apps use IDs, you can move to the most restrictive security level: safelisting with IDs only. This security level enforces the performance benefit of using IDs rather than full operation strings. If you're content with the safelisting aspect of persisted queries with only optional performance benefits, you don't need to enable it.

Coordinate with client teams

Once you've followed the implementation steps for one client, you can coordinate across all your client teams:

  1. Identify all the client apps that execute s against your , and the GraphQL client libraries that those apps use.
    • Before you enable safelisting in your , your client apps must start publishing their s to your PQL.
  2. Communicate to your client development teams that adopting persisted queries will require adding tooling to their CI/CD pipeline.
  3. Identify which team members will assist with adding tooling to their respective CI/CD pipelines.
Query List
Web client
Android client
iOS client

Guide each client team to follow the implementation steps presented in the incremental adoption path.

Manifest format

⚠️ This manifest format is subject to change during the preview period.

You only need to read this section if you're building your own tooling to generate persisted query manifests.

A persisted query manifest has the following minimal structure:

"format": "apollo-persisted-query-manifest",
"version": 1,
"operations": [
"id": "dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f",
"body": "query UniversalQuery { __typename }",
"name": "UniversalQuery",
"type": "query"

Manifest properties are documented below.

Top-level properties


This value is currently always apollo-persisted-query-manifest.


This value is currently always 1.


An array of objects describing the individual GraphQL s to publish.

For details, see Per-operation properties.

Per-operation properties

Each entry in a manifest's operations array is a JSON object that describes a single GraphQL to publish:

"id": "dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f",
"body": "query UniversalQuery { __typename }",
"name": "UniversalQuery",
"type": "query"

Each object has the following properties:


The unique ID to use for the in your PQL.

This value must be unique among operations in the PQL. It can match a previously-published as long as the operation's body remains the same. If you try to publish an with the same id as an existing but a different body, manifest publication throws an error. interprets this as an attempt to overwrite the existing PQL entry with a new body, which would change the behavior of existing deployed clients.

To ensure uniqueness, tooling should generate this value based on the body. For details, see Generating IDs.


The complete query document for the . Includes the definition of the operation itself, along with accompanying definitions. The executes this string as the query document when a client sends the corresponding ID or matching operation. For details, see Ensuring consistent operation documents.


The 's name. Must match the name specified in body.

This value does not need to be unique among s in the PQL. Often, different clients execute slightly different operations with the same name, and those operations each require a separate entry in the PQL.


The type of GraphQL . Always one of the following values:

  • query
  • mutation
  • subscription

Generating IDs

When generating IDs for a manifest, you should use a value that's unique to each operation, such as the query document's cryptographic hash. Apollo's manifest generation tools use the base16 representation of the document's SHA256 hash, which is the same format used for APQ.

By generating identifiers based on query documents this way, you ensure that different s always have different IDs. ID uniqueness prevents unexpected collisions in your PQL. It also allows the to execute queries both by full query strings and PQL-specified IDs.

Never use an operation's name for its PQL ID. Different clients (or even different versions of the same client) might execute different s with the same name, and all of those distinct s should be present in your PQL.

Ensuring consistent operation documents

Whenever a client sends an string to a with safelisting enabled, the router checks for that operation string's presence in its persisted query list.

When comparing an incoming freeform GraphQL document to the registered s in its PQL, the ignores some aspects of the document that have no semantic impact:

  • Ignored tokens such as white space, comments, and commas are ignored.
  • The order of top-level definitions ( and definitions) is ignored. This means that when assembling a full GraphQL document from its operation and fragments, there's no need to ensure that fragments are put in the same order at build time and at run time.

However, all other details of the document must match. For example, order, order, $variable names, names, string and numeric literals, and the presence of __typename s must match between the incoming freeform GraphQL document and the document in the persisted query list.

Note: Prior to v1.28, safelisting required the incoming document to match the document in the safelist precisely, including white space, comments, and top-level definition order.

For example, most applications treat responses from the following queries equivalently, but the would reject the client because it doesn't match the PQL entry exactly. (The operations do semantically differ because GraphQL servers return fields in the order requested, even though most applications ignore the order of object s in JSON.)

PQL entry
query GetBooks {
books {
Client operation
query GetBooks {
books {

Ordering differences (other than the order of top-level definitions) between a preregistered and the operation a client sends can similarly cause the to reject client operations, even if they have no semantic impact on the operation.

PQL entry
query GetBooks($limit: Int, $offset: Int) {
books(limit: $limit, offset: $offset) {
Client operation
query GetBooks($limit: Int, $offset: Int) {
books(offset: $offset, limit: $limit) {

The ignores top-level definition order and ignored tokens in order to make it easier to build tools that generate persisted query manifests whose contents match what will be sent at runtime. If your use case requires further normalization steps to be applied when comparing incoming opportunities to the safelist, contact Apollo Support; we are open to adding further normalization as an opt-in feature.

To ensure that you generate manifest entries correctly, it's important to note that your app's client library may modify the strings you define in your source before executing those corresponding operations. For example, by default, all Apollo Client libraries add the __typename to every object in a query if that field isn't already present:

Source-defined query
query GetBooks {
books {
Client-executed query
query GetBooks {
books {

The manifest generation tools for Apollo Client libraries all account for this default behavior.

If you're building your own manifest generation tool, ensure it accounts for any such changes in your chosen client library. Otherwise, the will reject your app's operations due to an operation string mismatch if safelisting is enabled.

Similarly, if your clients execute s by providing their PQL-specified ID, they might execute an operation without the augmentation added by your client library if you don't account for these operation changes.

Securing subgraphs
Cloud routing overview
Edit on GitHubEditForumsDiscord