Docs
Launch GraphOS Studio

Safelisting with persisted queries

Secure your graph while minimizing request latency


This feature is only available with a

.
You can test it out by signing up for a free
Enterprise trial
.

APIs are broadly open by design to provide flexibility and efficiency in client development. However, this openness also introduces the risk of potentially malicious requests and the subsequent need to secure your graph.

With

, you can enhance your 's security by maintaining a persisted query list () for your 's . To create and update the PQL, first-party apps register trusted to the PQL at build time.

First-party apps
Register
trusted operations
Web client
Android client
iOS client
Persisted
Query List

NOTE

Clients can register any kind of to a , including queries, , and .

At runtime, the

checks incoming requests against the , which can act as safelist, depending on your
router configuration
.

Supergraph
Registered
operations
Unregistered
operations
✅ Router executes
registered operations
❌ Router blocks
unregistered operations
Apollo Router
Graph
Router
Persisted
Query List
Subgraphs
Subgraph A
Subgraph B
Subgraph C
First-party apps
Web client
Android client
iOS client
Bad actor

Your can use its () to both protect your and speed up your clients' :

  • When you enable safelisting, your rejects any incoming not registered in its .

  • Client apps can execute an by providing its -specified ID instead of the entire operation string.

    • Requesting by ID can significantly reduce latency and bandwidth usage for large strings.
    • Your can require that clients provide by ID and reject full operation strings—even operation strings present in the .

Differences from automatic persisted queries

The also supports a related feature called

(). With APQ, clients can execute a by sending the SHA256 hash of its operation string instead of the full string.

has a few limitations compared to registered .

Automatic persisted queries Registered persisted queries
Operation performance✅ Clients can send identifiers instead of full operation strings, reducing request sizes and latency dramatically.🌟 Registered persisted queries share the same performance enhancement mechanism as APQs. Additionally, they benefit from query plan
cache warm-ups
, which are active by default from version
1.31.0
of the router.
Build- vs. runtime registrationOperations 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, addresses your use case. If you also want to secure your with safelisting, you should register operations in a .

Security levels

The supports the following security levels, 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 in its PQL. Clients can use either PQL-specified IDs or operation strings to execute operations.
Safelisting with IDs onlyClients can only execute operations by providing their PQL-specified IDs; the router rejects all freeform GraphQL requests.

💡 TIP

You can find more details, including configuration instructions, in the

.

These levels allow you to

on a client-by-client basis. Specifically, the should use audit mode until you're confident that all your clients' trusted have been registered in the . Refer to the
incremental adoption section
for a step-by-step guide.

Implementation steps

provide benefits to different teams:

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

Implementation also requires collaboration among these parties. These are the main steps for implementing 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. Operation registration
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 operation 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

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 , you first need a persisted query list () in . Platform teams create an empty PQL in GraphOS Studio so that client teams can register to it.

Each is associated or "linked" with a single in . A graph, however, can have several PQLs. For example, one graph may need multiple PQLs if you want a separate PQL for each

. You can link a to any of its . 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 page in
    GraphOS Studio
    , open the page for a by clicking its PQL button:
The persisted query list button in the Studio graph list

💡 TIP

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

  1. From the page:

    • If you haven't created any yet, click Create a Persisted Query List.
    • If you already have at least one , click New List in the upper right.
  2. In the dialog that appears, provide a name and (optional) description for your , then click Create.

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

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

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

  6. Click Finish to close the dialog and save your newly created .

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

💡 TIP

It's safe to link an empty or incomplete to a because your doesn't use its PQL for anything until you

(covered in a later step).

  1. From the table on your 's page, open the ••• menu under the Actions column for the you want to link. Click Link and Unlink Variants.

  2. In the dialog that appears, use the dropdown menu to select any you want to link your to.

💡 TIP

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

  1. Click Save.

After you link a to a , makes the PQL available in

, the service that delivers configurations to your at runtime. Once
configured
, the polls Uplink to ensure it uses the most up-to-date for linked .

2. Router configuration

The is the key component that enforces safelisting.

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

  1. Ensure your instances are ready to work with :

    • Make sure you're using version 1.32.0 or later of the . (The feature was released in
      preview
      in version 1.25.0 and made
      generally available
      in 1.32.0.)
    • Make sure your instances are
      connected to your GraphOS Enterprise organization
      and that they're associated with a that your is linked to.
  2. Set your desired security level in your 's YAML config file. For supported options, see

    . When first implementing , it's best to start with
    audit—or "dry run"—mode
    .

  3. Deploy your updated instances to begin using your .

Once your organization's has registered all your clients' and you've ensured your client apps are only sending registered operations, you can update your configuration to the

.

Router security levels

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

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

When adopting , you should start with a less restrictive security such as

. 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

for option details.

NOTE

From version 1.25.0 to 1.32.0, the persisted_queries configuration option was named preview_persisted_queries. Upgrade your to version 1.32.0 or later to use the

version of the feature and the example configuration snippets below.

Allow operation IDs

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

router.yaml
persisted_queries:
enabled: true

NOTE

You can use this security level with or without

enabled.

This mode lets clients execute by providing their -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 you need to either add to your or stop your client apps from making.

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

router.yaml
persisted_queries:
enabled: true
log_unknown: true

NOTE

You can use audit mode with or without

enabled.

Unregistered appear in your

.

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 registered in the , no log message will be output.

You can use these logs to audit sent to your router and ask client teams to

to your if necessary.

Safelisting

⚠️ CAUTION

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

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

router.yaml
persisted_queries:
enabled: true
log_unknown: true
safelist:
enabled: true
require_id: false
apq:
enabled: false # APQ must be turned off

NOTE

To enable safelisting, you must turn off

(). APQs let clients
register arbitrary operations at runtime
while safelisting restricts to those that have been explicitly registered.

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

.

💡 TIP

It's best to keep log_unknown as true while adopting safelisting so you can monitor the your rejects. 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

⚠️ CAUTION

Do not start with this configuration. It requires all your clients to execute by providing their -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 registered operation IDs:

router.yaml
persisted_queries:
enabled: true
log_unknown: true
safelist:
enabled: true
require_id: true
apq:
enabled: false # APQ must be turned off

NOTE

To enable safelisting, you must turn off

(). APQs let clients
register arbitrary operations at runtime
while safelisting restricts to those that have been explicitly registered.

If you want to use this security level, you should always first set up

. ID-only safelisting requires all your clients to execute via -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

all rejected , including those registered to your but that used the full operation string rather than the PQL-specified ID.

NOTE

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

3. Operation registration

Registering to a has two steps:

  1. Generating () using client-specific tooling
  2. Publishing to the using the tool

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

3.1 Generate persisted query manifests

Once a exists in , client teams can start publishing to it. To do so, you must generate JSON manifests of the operations 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 when you release a new client app version.

Generation methods

for Web, Kotlin, and iOS each provide a mechanism for generating a manifest file from your app source. Apollo also supports manifests

.

NOTE

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

.

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 development 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 you expect it to), you can configure the command's behavior using the options described in the
      package README
      .

.

Apollo Kotlin

NOTE

Manifest generation requires 3.8.2 or later.

To generate an manifest with , you modify your project's

to generate a manifest in addition to the standard Kotlin source for your :

apollo {
service("myapi") {
packageName.set("com.example.myapi")
operationManifestFormat.set("persistedQueryManifest")
}
}

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

.

Apollo iOS

NOTE

Manifest generation requires 1.4.0 or later.

To generate an manifest with , you use the same

that you use to generate Swift code for each of your . Specifically, you modify the engine's file output configuration to include the output of an operationManifest.

.

Relay compiler

The has a

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

💡 TIP

Ensure your version is 0.17.2 or later. Previous versions of don't support publishing to a .

After you

, you publish it to your 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 is
    linked to
    .
    • have the format graph-id@variant-name.
  • Use the --manifest option to provide the path to the manifest you want to publish.

NOTE

The persisted-queries publish command assumes manifests are in the

generated by tools. The command can also support manifests
generated by the Relay compiler
by adding the
--manifest-format relay
. Your version must be 0.19.0 or later to use this argument.

The persisted-queries publish command does the following:

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

    • Publishing a manifest to a 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 , the entire publish command fails with an error.
  2. Updates any other that the is applied to so that associated with those variants can fetch their updated PQL.

As with

, it's best to execute this command in your CI/CD pipeline to publish new 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 's data in , and are appropriate for sharing with trusted third party client developers who should be allowed to publish to your graph's but should not otherwise have access to your graph.

Test operations

You can send some test 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 (https://go.apollo.dev/elv2)
2023-05-11T15:32:30.684480Z INFO Anonymous usage data is gathered to inform Apollo product development. See https://go.apollo.dev/o/privacy for details.
2023-05-11T15:32:31.507085Z INFO Health check endpoint exposed at http://127.0.0.1:8088/health
2023-05-11T15:32:31.507823Z INFO GraphQL endpoint exposed at http://127.0.0.1:4000/ 🚀

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

curl http://localhost:4000 -X POST --json \
'{"extensions":{"persistedQuery":{"version":1,"sha256Hash":"dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f"}}}'

If your 's 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

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

NOTE

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

.

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

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

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

💡 TIP

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

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

With Web, sending by ID requires you to use an

at runtime alongside @apollo/client's built-in createPersistedQueryLink. 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 and, thus, don't need additional support.

Refer to the Web's

for implementation details.

Incremental adoption path

'

let you adopt an incremental approach rather than simultaneously requiring all clients to send requests via registered IDs. You can follow these steps for incremental adoption:

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

  2. Follow all

    for your chosen client:

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

If safelisting is your goal, you'll need to

to complete these steps for each of your client apps.

Once your 's logs are completely clear of unexpected , you can configure your router to use

. Then, to reap the performance benefits,
update your client apps
to use IDs rather than full operation strings.

Once you've confirmed all client apps use IDs, you can move to the most restrictive security level:

. This security level enforces the performance benefit of using IDs rather than full operation strings. If you're content with the safelisting aspect of 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 against your , and the libraries that those apps use.
    • Before you enable safelisting in your , your client apps must start publishing their to your .
  2. Communicate to your client development teams that adopting 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.

GraphOS
CI/CD
Publishes
operations
Publishes
operations
Publishes
operations
Persisted
Query List
Web client
build
Android client
build
iOS client
build

Guide each client team to follow the implementation steps presented in the

.

Persisted query list management

From the Persisted Query Lists page, you can perform the following actions by clicking the ••• menu under the Actions column on the right of any :

  • Download the list as a JSON file
  • Publish s
  • Update the 's name and description
  • Link and unlink s
  • Delete the entirely

The Publish operations action provides the

.

Operation management

You can add new to a by using the

, and you can delete from the 's page in . Since every operation should
have a unique ID
, you can't update existing —if you try to publish a revised operation
body
for an existing ID, returns an error.

Adding operations

To add new to a , you need to

. If the manifest you're publishing doesn't include an in the , this does not delete that operation from the PQL. Each manifest publication only adds any new operations to the PQL.

Deleting operations

If you want to delete an , you must do so from Studio. From the Persisted Query Lists page, click a to open it. Then, click the ••• menu under the Actions column next to a particular and select Delete.

Persisted query list operation actions in Studio

You should only delete that are problematic and shouldn't be executed. Even for such operations, it's important to recognize that operation deletions can cause application errors for legitimate clients, depending on your 's

and if the client is sending full strings or operation IDs.

  • Regardless of security level, if a client sends an ID of a deleted operation, the rejects the operation.
  • If you enabled
    safelisting
    , the rejects any you deleted from the but that your clients still perform, whether clients send operation IDs or full operation strings for the deleted operation.
  • When the
    allows operation IDs
    or is in
    audit mode
    , if a client sends the full string of a deleted operation, the executes it.

Although you can't undo deletions directly via the Studio UI, you can always republish a deleted operation using

.

Manifest format

NOTE

You only need to read this section if you're building your own tooling to

.

A has the following minimal structure:

persisted-queries-manifest.json
{
"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

PropertyDescription
format

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

version

This value is currently always 1.

operations

An array of objects describing the individual to publish.

For details, see

.

Per-operation properties

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

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

Each object has the following properties:

PropertyDescription
id

The unique ID to use for the in your .

This value must be unique among in the . It can match a previously published operation as long as the operation's

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 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

.

body

The complete 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

.

name

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

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

type

The type of . 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 's crypto hash. Apollo's

use the base16 representation of the 's SHA256 hash, which is the same format used for
APQ
.

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

⚠️ CAUTION

Never use an 's name for its ID. Different clients (or even different versions of the same client) might execute different operations with the same name, and all those distinct operations 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 .

When comparing an incoming freeform to the registered in its , the ignores some aspects of the 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 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 must match. For example, order, order, $variable names, names, string and numeric literals, and the presence of __typename must match between the incoming freeform and the document in the .

NOTE

Prior to v1.28, safelisting required the incoming 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 entry exactly. (The operations do semantically differ because

, even though most applications ignore the order of object in JSON.)

PQL entry
query GetBooks {
books {
publishDate
title
}
}
Client operation
query GetBooks {
books {
title
publishDate
}
}

Ordering differences (other than the order of top-level definitions) between a registered 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) {
title
}
}
Client operation
query GetBooks($limit: Int, $offset: Int) {
books(offset: $offset, limit: $limit) {
title
}
}

NOTE

The ignores top-level definition order and ignored tokens to make it easier to build tools that generate whose contents match what will be sent at runtime. If your use case requires further 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 libraries add the __typename to every object in a if that field isn't already present:

Source-defined query
query GetBooks {
books {
author
title
}
}
Client-executed query
query GetBooks {
books {
author
title
__typename
}
}

NOTE

The

for 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 by providing their -specified ID, they might execute an operation without the augmentation added by your client library if you don't account for these operation changes.

Previous
Securing subgraphs
Next
Using @defer
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company