Configuration reference
A service is configured by one file. This page lists everything it accepts.
The file
Section titled “The file”You write it as gpilot.toml in the directory you deploy from. gpilot deploy picks it up
automatically, along with schema.graphql beside it.
TOML is canonical, but the CLI also accepts YAML, JSON and JSON5, chosen by extension
(gpilot.yaml, gpilot.yml, gpilot.json, gpilot.json5, gpilot.tml) and converts them to
TOML on the way up. gpilot config convert does the same conversion locally, and
gpilot config get prints what the service is currently deployed from.
Every setting has a default, and an empty file is valid. Configure what you need to change. A deployment without a configuration file runs on the defaults alone; see what goes into a deployment.
[request]
Section titled “[request]”What the edge accepts, and how much GraphQL it is willing to parse. Not every limit here runs on every request:
- On every request, a cache hit included:
graphql_paths, which decides whether a request is GraphQL at all and refuses a GraphQL operation sent anywhere else, andmax_body_bytes. Authentication and the client profile checks also run before the cache. - Only on requests that reach GraphQL processing, which means a cache miss or a pass:
max_query_depth,max_document_bytes,max_selections,introspectionandintrospection_cache_max_age. A hit is answered without parsing the document, so these limits cost it nothing. They protect your origin, which a hit does not reach.
| Key | Type | Default | Does |
|---|---|---|---|
request. | list of strings | ["/graphql"], or [] without a schema | The paths treated as GraphQL. Several paths are allowed (for example the old and the new path during a migration); all of them serve the service’s one schema, so a second API with a schema of its own is a second service. Matched the way your origin’s router would: case does not matter, repeated slashes, a trailing slash, . and .. segments, percent-escapes (except %2F) and ;parameters are ignored, and a path below an entry counts as the entry (/graphql/x is /graphql, / is not), because that is how Express mounts Apollo Server and Yoga. An entry of / matches the root only. Anything else is forwarded as ordinary HTTP, except a request that carries a GraphQL operation there: that is refused with 404 and NOT_ while any GraphQL check is in force (the introspection block, persisted_query., a client profile with persisted_query = "required", or a depth, document size or selection limit, on the service or a profile), which by default it is. See GraphQL on other paths. Serve GraphQL only on these paths all the same: an upload or a WebSocket subscription sent elsewhere is not recognized. An empty list is taken at its word: GraphQL nowhere, and nothing is refused for being GraphQL. A deployment without a schema always runs with []: setting the key there (any value, [] included) does not fail the deployment, the key is ignored, and the build records a request_ error saying so. |
request. | integer (bytes) | 8388608 (8 MiB) | Largest request body accepted, inclusive. A larger one is refused with 413 and PAYLOAD_ before it is parsed and before it reaches your origin. Raise or lower it with any integer. The limit cannot be removed: TOML has no null, and gpilot refuses a null in a YAML or JSON file. 0 is not “no limit” here: it refuses every request that carries a body. |
request. | list of "batch", "multipart", "other" | [] | The kinds of body on a GraphQL path that are forwarded to your origin unchecked instead of refused with 415 and UNSUPPORTED_. batch is a JSON array of operations, multipart a multipart/ upload, other anything else (text, no content type, JSON that names no operation, a body that does not parse). A listed kind reaches your origin as the bytes the client sent, past persisted query enforcement, the introspection block and the document limits. A request without a body is never refused here. A compressed body (any Content- other than identity) is refused with 400 and INVALID_, not forwarded, whichever kinds are listed. See bodies on a GraphQL path. |
request. | integer | 10 | Deepest nesting allowed in an operation a client sent. 0 disables the limit. |
request. | integer | 10 | The same for a document resolved from the persisted query registry. 0 disables it, which is only safe with persisted_query.: under apq any client can register a document and then send its hash. |
request. | integer | 65536 (64 KiB) | Longest GraphQL document parsed, in bytes of the query string, inclusive. A longer one is refused with 400 and DOCUMENT_ before it is parsed. Governs a document the request carried; a registered one is measured against persisted_query., which inherits this value unless you set it. An APQ registration over the registry’s ceiling is not stored. 0 disables the limit. |
request. | integer | 4000 | Most selections an operation may make, inclusive, counted with every alias and every fragment spread expanded. More is refused with 400 and DOCUMENT_ before the query plan is built. Governs a document the request carried; a registered one is measured against persisted_query., which inherits this value unless you set it. 0 disables the limit. See how selections are counted. |
request. | "blocked" | "allowed" | "blocked" | Whether the edge answers __schema and __type introspection. Blocked by default so a public service does not hand out its schema shape. __typename is never blocked. |
request. | integer (seconds) | unset | Accepted and has no effect. It used to give the response to an introspection request GraphPilot itself sends a cache lifetime; such a platform probe is now never cached, so it always reads what your origin answers. A configuration that sets it still deploys. |
[request]graphql_paths = ["/graphql", "/api/graphql"]introspection = "allowed"forward_unchecked_bodies = ["multipart"] # this API takes file uploads
[request.max_query_depth]client_document = 8See limits for what each limit protects against.
[origin]
Section titled “[origin]”How the request is sent to your API. The origin URL is not here: it belongs to the service
and is changed with gpilot service edit.
| Key | Type | Default | Does |
|---|---|---|---|
origin. | string | "__gp_" | The prefix on fields selection set expansion injects, so they can be recognized and stripped again. A client document whose alias (or unaliased field name) starts with it is refused with 400 RESERVED_. Change it only if it collides with your own field names: a schema field starting with the prefix can no longer be selected without an alias. An empty or blank value means the default __gp_. |
origin. | "verbatim" | "query_ | "post_ | "verbatim" | Rewrites the HTTP method on the way to your origin. See below. |
origin. | integer or unset | 1000 | How long to wait for TCP and TLS. Capped at 5000 on every plan. |
origin. | integer or unset | 15000, or your plan’s cap if lower | How long to wait for the first response byte. Plan-dependent; see origin timeouts. |
origin. | integer or unset | 10000 | How long to wait between two bytes of a response already in flight. Capped at 60000. |
origin. | string or unset | unset | A header name your origin receives the GraphPilot request id under, so your own logs can be joined to GraphPilot’s. You choose the name, typically one your origin already logs, such as x-request-id. A name in the gp- namespace refuses the deployment, because that namespace never reaches your origin; so does a name that is not a valid HTTP header name. Do not reuse the name of your telemetry.: GraphPilot’s id replaces the client’s value under that name. See request ids. |
origin. | boolean | false | Whether your origin receives the client’s IP address, in the gp-client-ip request header. Off by default: an end user’s IP address is personal data, so passing it on is your decision. On, the value is the address the edge saw for the connection (on a shielded request, the one the first edge location saw, never a GraphPilot address), and no header is sent when the address is unknown. A client cannot set it: every gp- header a client sends is dropped on arrival. Independent of telemetry., which decides what GraphPilot stores, not what your origin receives. See request headers your origin receives. |
origin. | "strip" | "pass" | "strip" | What your origin receives of the forwarding headers a client sent: every header whose name starts with X-Forwarded-, plus Forwarded, X-Real-IP, X-Client-IP, Client-IP, True-, CF-, Fastly-, X-, X- and X-. "strip" removes them right before the request is sent to your origin; GraphPilot’s own policies and vary rules still read them, and so does the capture of the client’s request headers in telemetry. "pass" forwards them unchanged, and then trusting them and keying the cache on them is up to you. The client’s address your origin can trust is gp-client-ip (origin.). An [origin. only list that keeps one of these headers, or the pattern x-, while they are stripped refuses the deployment. See forwarding headers. |
origin. | string or unset | unset | The shield location that cache misses go through before they reach your origin, named <region>- (see the location table). Off by default, and available on paid plans. On Glider the deployment still succeeds, with a plan warning in its findings: the shield is configured but inactive, and it turns on by itself after an upgrade, without a redeploy. A value that is not a location the network knows refuses the deployment and suggests the closest match. |
A timeout above your plan’s ceiling or below 100 ms, or a retry policy whose worst case does not
fit 120 seconds, refuses the deployment. See a value out of bounds is
refused and timeouts and
retries.
The QUERY method (draft-ietf-httpbis-safe-method-w-body) is a safe, idempotent method that
carries a body: a read with a body, unlike GET. GraphPilot accepts it from clients and caches
it exactly like the same operation over POST. method_rewrite lets the two sides adopt it
independently:
| Value | Effect |
|---|---|
verbatim | Forward the method unchanged. |
query_ | Downgrade QUERY to POST, so clients can adopt QUERY while your origin stays on POST. |
post_ | Upgrade POST to QUERY, so your origin can adopt QUERY while clients keep sending POST. Only query operations are upgraded; mutations and subscriptions stay POST, because QUERY is a safe method and must not carry them. |
[origin.retry]
Section titled “[origin.retry]”Off unless you opt in. Full detail on origin retries.
| Key | Type | Default | Does |
|---|---|---|---|
origin. | boolean | false | Whether failed origin requests are retried at all. |
origin. | integer | 1 | Additional attempts. 1 means the origin sees the request at most twice. |
origin. | list of integers | [502, 503, 504] | Origin statuses that trigger a retry. |
origin. | boolean | true | Whether a transport failure is retried. |
origin. | boolean | false | Whether GraphQL mutations are retried. Turn on only if your origin never returns a gateway status after a write already executed, or your mutations are idempotent. Does not apply to plain HTTP requests. |
origin. | boolean | false | Whether plain HTTP requests with a method other than GET, HEAD or OPTIONS (POST, PUT, PATCH, DELETE and any other) are retried. Turn on only for an origin that makes its writes idempotent. Does not apply to GraphQL requests. |
origin. | "none" | "fixed" | "exponential" | "fixed" | How the delay is computed. |
origin. | integer | 1000 | Base delay. |
origin. | integer | 2000 | Ceiling for any single delay, for both fixed and exponential. |
A plain HTTP request is retried only when its method is GET, HEAD or OPTIONS; any other
method is sent once unless retry_unsafe_methods is true. See which requests are
repeated.
[origin.headers] and [origin.cookies]
Section titled “[origin.headers] and [origin.cookies]”Names dropped from the request on its way to your origin, after the cache decision has already been made from the request as the client sent it. Full detail on header and cookie filters.
| Key | Type | Default | Does |
|---|---|---|---|
origin. | list of strings | unset | Header names removed before the request reaches your origin. |
origin. | list of strings | unset | Header names that survive; everything else is removed. Mutually exclusive with hide. |
origin. | list of strings | unset | Cookie names removed from the Cookie header. |
origin. | list of strings | unset | Cookie names that survive in the Cookie header; everything else is removed. Mutually exclusive with hide. |
[origin.headers]hide = ["x-internal-debug"]
[origin.cookies]hide = ["internal_ab_test"]Setting both hide and only on the same section, or naming a protected name (host,
content-type, content-encoding, content-length, transfer-encoding, or anything starting
with gp-), refuses the deployment. See protected names.
So does a conditional rules list or when, which only [response.headers] and
[response.cookies] accept. A rules list is refused with
the reason, and the message closes with [origin.headers] supports only the plain `hide` and `only` lists (or
[origin.cookies]); a bare when is refused as an unknown key.
[cache]
Section titled “[cache]”| Key | Type | Default | Does |
|---|---|---|---|
cache. | boolean | true | Lets an origin Cache- that constrains neither storage nor freshness (a bare private, public or no-transform) yield to the schema’s @cacheControl policy instead of deciding the response itself. A private it stated is still carried over, so the entry stays partitioned. Set false to restore the older precedence, where the header’s mere presence decided. See precedence. |
[cache.request_directives]
Section titled “[cache.request_directives]”What the Cache-Control directives a client sends on its own request do. Each value is
"honor" or "ignore"; any other value, or an unknown key in this section, is refused when the
file is read. Full detail on the client’s own
Cache-Control.
| Key | Type | Default | Does |
|---|---|---|---|
cache. | "honor" | "ignore" | "honor" | Honored: a request carrying no-store is still answered from the cache when it can be, but its response is not written, reported as CACHE_. |
cache. | "honor" | "ignore" | "ignore" | Honored: a request carrying no-cache or max-age=0 skips the lookup, goes to your origin and is not written back, reported as PASS with CACHE_. Ignored by default because any caller could then send every request past the cache to your origin. |
[cache.request_directives]no_cache = "honor" # only for callers you trust[cache.http_key]
Section titled “[cache.http_key]”What the cache key of a plain HTTP request is made of: any request that is not a GraphQL
operation. By default it is the method, the path and the query string, plus a SHA-256 of the body
when there is one, so GET /products/42?color=blue is stored under GET:/products/42?color=blue
whichever hostname it arrived on. The same key is the ck: key you purge one response by, and
the cache.key in request telemetry. GraphQL keys are not affected. An unknown value, or an
unknown key in this section, is refused when the file is read. See the cache
key.
| Key | Type | Default | Does |
|---|---|---|---|
cache. | "exclude" | "include" | "exclude" | Whether the scheme and hostname are part of the key. Excluded, one path is one entry across the service hostname and every custom domain, and you purge it as ck:GET:/. Included, the key is the full URL, GET:https:/, and each hostname keeps its own entry. Include it only when your origin answers the same path differently per hostname. |
[cache.http_key]host = "include" # only when your origin serves different content per hostnameChanging the setting changes every plain HTTP key, so the entries stored under the old ones are no longer found: each misses once, is filled again under the new key, and the old entry expires on its own lifetime.
[cache.policy]
Section titled “[cache.policy]”How the cache is partitioned. Every value except downstream is a policy
expression; the concepts are on identifying callers.
| Key | Type | Default | Does |
|---|---|---|---|
cache. | expression | unset | The value a shared entry is partitioned by, for example one of your customers. |
cache. | expression | unset | The value a private entry is partitioned by. Without it, a private response is not cached. Write it so it identifies one caller, not their group. |
cache. | expression | unset | When it matches, the request skips the cache entirely and its response is not stored. Evaluated as a truth, not as a value. |
cache. | "off" | "if_ | "always" | "off" | Whether to emit a Cache- header on the response sent to the client. See downstream header. |
cache. | list of integers | [] | The 4xx and 5xx statuses that may be stored, under the ordinary rules, when your origin marks them cacheable. Any other error status is not stored, whatever its Cache- says, and reports PASS with CACHE_. A value outside 400 to 599 refuses the deployment. See an error status is not stored unless you opt in. |
cache. | list of strings | ["OPTIONS"] | Request methods that skip the cache entirely: no lookup, no key, no write back. Reported as CACHE_. Empty the list to let CORS preflights be answered at the edge; read the hazard first, since a preflight is only safe to cache because GraphPilot keys it on the caller’s Origin and on what it asked for. Names are capitals, and one that is not a valid HTTP method is refused when the file is read. |
[[cache.policy.vary]]
Section titled “[[cache.policy.vary]]”Repeatable. Each entry derives a value and writes it onto the request as a header. Every
response is partitioned on it, plain HTTP included, whether or not your origin’s Vary names
it. An entry marked scope_only partitions a response your schema decided only when its scope
names it in @vary; a response whose lifetime your origin’s Cache-Control decided is still
partitioned on it.
A request without a credential that the expression does not resolve for is stored as the variant
without the header. A request with a credential that it does not resolve for, or resolves empty
for, is not stored (CACHE_SKIPPED_PRIVATE_WITHOUT_DISCRIMINATOR). Each entry costs hit rate, so
configure only those whose value shapes a response. See vary rules.
| Key | Type | Default | Does |
|---|---|---|---|
name | string | required | The header the derived value is written to. Configuring an entry claims that name: a client sending it has its value removed, logged as INGRESS_. Should not start with gp-: such a name is not refused and still partitions the cache, but the namespace is stripped before your origin, so pass_ has no effect. A name that is not a legal header name is skipped and reported in the deployment’s diagnostics. |
value | expression | required | Computes the value. |
pass_ | "never" | "anonymized" | "plain" | "never" | What your origin receives. anonymized sends a keyed digest, so the origin can tell two contexts apart without learning what distinguishes them. Under anonymized, request telemetry records that same digest as the segment value. Under never and plain, telemetry. decides: a keyed pseudonym by default, the value in clear only if you choose it. See what Insights records. |
scope_only | boolean | false | Leaves the entry to your schema’s named scopes: a response your schema decided is partitioned on it only when its scope names it. Only accepted with pass_to_origin = "never"; any other combination refuses the deployment. See vary rules. |
[cache.policy]private = "auth:idp.sub"downstream = "if_cacheable"
[[cache.policy.vary]]name = "x-segment"value = "auth:idp.plan"pass_to_origin = "anonymized"[cache.headers] and [cache.cookies]
Section titled “[cache.headers] and [cache.cookies]”What the cache does with response headers and cookies when it writes an entry. Full detail on header and cookie filters.
| Key | Type | Default | Does |
|---|---|---|---|
cache. | list of strings | unset | Response headers not written to the entry. The client that caused the miss still receives them; a later hit does not. Refused at deploy time if it names a protected header (content-type, content-, cache-, vary, surrogate-). |
cache. | list of strings | unset | Headers that no longer refuse storing the response, out of the fixed list GraphPilot otherwise refuses on (WWW-, Proxy-, Authentication-, Proxy-, Clear-, Set-Login). An allowed header is also never written to the entry. Refused at deploy time if it names anything other than one of those six, and refused too for Set-Cookie or Set-Cookie2, which [cache. governs instead; use cache. to drop an ordinary header from the cache entry. |
cache. | list of strings | see the default list | Cookie names the service says carry no session: they do not make a request credentialed, do not refuse a response for carrying Set-Cookie, and are never written to the entry. Setting this key replaces the default list rather than adding to it. |
[cache.headers]exclude = ["x-request-id"]
[cache.cookies]allow = ["GAESA", "GCLB", "my_custom_lb_cookie"]Neither section accepts a conditional rules list or when. Writing one refuses the
deployment, because what is stored under a cache key cannot depend on a condition the key does
not include. The message says so and names the plain lists the section does read, closing with
[cache.headers] supports only the plain `exclude` and `allow` lists, or
[cache.cookies] supports only the plain `allow` list.
[response] reporting headers
Section titled “[response] reporting headers”Whether a client sees the headers GraphPilot sets to report what the cache did. Changing one changes only what leaves: caching, purging and telemetry are unaffected. Every header is listed on headers; full detail on switching the headers on and off.
| Key | Type | Default | Does |
|---|---|---|---|
response. | boolean | true | Sends the cache status family: gp-cache, gp-, gp-, gp-cache-age, gp- and gp-. Set false to withhold all six. |
response. | boolean | false | Sends gp-, the surrogate keys the response is tagged with, plus gp- when keys were left out. The keys are stored and purging by key works either way. |
response. | integer (bytes) | 8192 | The most the value of gp- may carry, whole keys only, cache and root field keys first. Applies to that one header, not to all headers together. Keys that do not fit are counted in gp-; if none fits, gp- is left off. From 1 to 128000 (Fastly’s per-header limit); anything else refuses the deployment. Only read when surrogate_ is true. See size limit. |
response. | boolean | false | Sends gp-, the id GraphPilot recorded this request under, shown as the Request ID column in Insights and as Entry ID in the request’s Overview tab. The id is minted and the telemetry row carries it either way; this decides only whether the caller sees it. See request ids. |
[response]cache_headers = falsesurrogate_key_header = truesurrogate_key_header_max_bytes = 32000request_id_header = truecache_headers and surrogate_key_header are the only way to withhold a gp- header:
[response.headers] refuses the namespace at deploy time.
[response.cors]
Section titled “[response.cors]”CORS headers on the responses GraphPilot produces itself: its rejections and its persisted query answers, which never reach your origin and so carry none of the CORS headers your origin would send. Without this section, nothing changes. Full detail on edge rejections.
| Key | Type | Default | Does |
|---|---|---|---|
response. | list of strings | [] | The origins whose requests get CORS headers on a response GraphPilot produced, written the way a browser sends Origin: scheme:/, with a port where it is not the scheme’s default, over http or https, with no path and no trailing slash. Matched case-insensitively. "*" on its own allows every origin. |
response. | boolean | false | Adds Access-, for a browser app that sends cookies or uses credentials: "include". |
"*" combined with another origin or with allow_credentials = true, a partial wildcard such as
https://*.example.com, an origin with a path, and null refuse the deployment. Responses
from your origin, cache hits included, are never changed.
[response.headers] and [response.cookies]
Section titled “[response.headers] and [response.cookies]”Names dropped from the response on its way to your client, applied on delivery rather than when an entry is written, so it also governs a cache hit served from an entry stored before this configuration existed. Full detail on header and cookie filters.
| Key | Type | Default | Does |
|---|---|---|---|
response. | list of strings | unset | Header names removed from the response before it reaches your client. |
response. | list of strings | unset | Header names that survive; everything else is removed. Mutually exclusive with hide. |
response. | list of strings | unset | Cookie names removed from Set-Cookie and Set-Cookie2. |
response. | list of strings | unset | Cookie names that survive in Set-Cookie and Set-Cookie2; everything else is removed. Mutually exclusive with hide. |
response. | array of tables | unset | Lists that apply only to the requests their when matches. See below. |
response. | array of tables | unset | The same for cookies. |
[response.headers]hide = ["x-origin-latency-ms"]
[response.cookies]hide = ["__internal_session"][[response.headers.rules]] and [[response.cookies.rules]]
Section titled “[[response.headers.rules]] and [[response.cookies.rules]]”Repeatable. Applied after the plain list and after one another, so a name survives only if every list that applies keeps it. Full detail on conditional rules.
| Key | Type | Default | Does |
|---|---|---|---|
hide | list of strings | unset | Names this rule drops. |
only | list of strings | unset | Names this rule keeps; everything else is dropped. A rule must set exactly one of hide and only. |
when | expression | unset | Whether the rule applies to this request. Evaluated as a truth, with auth: claims available. Absent means every request. |
[[response.headers.rules]]hide = ["x-debug-trace"]when = "!auth:idp.role || auth:idp.role != 'admin'"rules and when are accepted only here. [origin.*] and [cache.*] refuse them at
deploy time, because their filtering reaches the cache under a key that does not include the
condition.
Setting both hide and only on the same section, or naming a protected name (content-type,
content-encoding, cache-control, vary, surrogate-key, or anything starting with gp-),
refuses the deployment. See protected names.
[auth.providers.<name>]
Section titled “[auth.providers.<name>]”Repeatable, one block per provider. The block name is what a policy expression refers to
(auth:<name>.<claim>). Full detail on authentication.
JWT providers (type = "jwt"):
| Key | Type | Default | Does |
|---|---|---|---|
type | "jwt" | required | |
allowed_ | list of strings | required | The algorithms this provider accepts. A security control, not a formality: it is what stops an algorithm downgrade. Accepted spellings: HS256, HS384, HS512, RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES256K, EdDSA and BLAKE2B. |
jwks_url | string | unset | Where the issuer publishes its keys. The key set is read at deploy time and delivered to the edge. When a token names a key id the delivered set lacks, the edge location fetches jwks_url (at most once a minute, or 10 seconds after a failed fetch) and shares the result, so a rotation is usually picked up without a redeploy. Only if that fetch fails is the token rejected with AUTH_. See authentication. |
public_key | string (PEM) | unset | Public key for an asymmetric algorithm without a JWKS. |
default_ | string | unset | Key to use when a token carries no kid. |
secret | string | unset | Shared secret, for symmetric algorithms. Stored and deployed to the edge as written, as part of the configuration document: anyone with service:edit, a member or an API key, can read it back with gpilot config get. Keep a gpilot.toml that holds a secret out of version control, or use an asymmetric algorithm with jwks_url or public_key, which hold only public material. |
header | string | "Authorization" | Header carrying the token. A Bearer prefix is stripped when present. |
allowed_ | list of strings | required with jwks_url, public_key or an asymmetric algorithm, otherwise unset | Accepted iss values. A provider that verifies with a public key (it sets jwks_url or public_key, or lists an RS*, PS*, ES*, ES256K or EdDSA algorithm) is refused without it, or with an empty list, and the deployment fails. On a shared-secret provider, unset accepts any issuer and the deployment warns (auth_). Set it. |
allowed_ | list of strings | unset | Accepted aud values. Unset accepts any audience, and the deployment warns (auth_). Set it. |
required_ | string | unset | Required nonce. |
required_ | string | unset | Required kid. |
time_ | duration | unset | Clock skew allowance. See the note below on how to spell a duration. |
max_validity | duration | unset | Reject tokens issued longer ago than this, whatever their expiry. |
max_ | integer | unset | |
max_ | integer | unset | |
accept_ | boolean | false | Whether a token whose nbf is in the future is accepted. |
allow_ | boolean | false | Whether a token without an exp claim is accepted. By default it is rejected with AUTH_. Turn it on only for an issuer that mints tokens without exp, and set max_validity with it. A token that states an expiry is held to it either way. |
API key providers (type = "api_key"):
| Key | Type | Default | Does |
|---|---|---|---|
type | "api_key" | required | |
prefixes | list of strings | required, non-empty | The prefixes marking a value as this provider’s credential. An empty list, or one containing an empty string, is refused. |
header | string | "Authorization" | Header carrying the key. |
[auth.rejection]
Section titled “[auth.rejection]”What happens to a credential that cannot be used, per reason. Letting a request continue is not
the same as trusting it: the provider contributes no claims, so a private policy naming it
yields no discriminator and the response is not cached.
| Key | Type | Default | Does |
|---|---|---|---|
auth. | "reject" | "continue" | "reject" | The signature did not verify, the token expired, or a required claim did not match. |
auth. | "reject" | "continue" | "reject" | The token names a key id GraphPilot does not have. |
auth. | "reject" | "continue" | "reject" | No key material at all for this provider. |
[auth.providers.idp]type = "jwt"allowed_algorithms = ["RS256"]jwks_url = "https://idp.example.com/.well-known/jwks.json"allowed_issuers = ["https://idp.example.com/"]
[auth.rejection]invalid_token = "reject"unknown_key_id = "continue"key_material_unavailable = "continue"[persisted_query]
Section titled “[persisted_query]”Four independent switches, plus a preset that names the common combinations. See choosing a
setup for how to choose.
| Key | Type | Default | Does |
|---|---|---|---|
persisted_query. | "off" | "apq" | "trusted_ | "trusted" | "off" | Sets the four switches below to a named combination (see the table after this one). A switch you write beside it overrides the preset’s value for that switch. mode is accepted as an alias of preset. |
persisted_query. | boolean | false | Whether a hash is resolved against the registry at all. Off, a request carrying a hash is answered with PERSISTED_. |
persisted_query. | boolean | false | Whether a client may register an operation by sending it with its hash (Automatic Persisted Queries). Anyone who can call the service can then write to its registry. Off, a push is the only way in. |
persisted_query. | boolean | false | Makes the registry an allowlist: an operation it does not hold is refused with 403 and FORBIDDEN, including a full query sent as text, and logged as PERSISTED_. Fails closed when the registry cannot be read, again as FORBIDDEN, logged as PERSISTED_. Checked below the cache, so on misses and passes: a response cached before you switched it on is served until it expires or is purged. |
persisted_query. | boolean | false | Reports each operation the registry does not hold as PERSISTED_, and lets it run. The list of what your manifest is still missing. |
persisted_query. | integer | 604800 (7 days) | How long a resolved document stays in the edge-location cache. Entries are content-addressed, so a hash points at one document forever or at nothing. |
persisted_query. | integer | 60 | How long a “this hash resolves to nothing” answer is remembered, so random hashes cannot force a lookup per request. |
persisted_query. | integer (bytes) | request. | The document ceiling for a registered document: one resolved from the registry for a hash, and one a client registers over APQ. Silence inherits the [request] value, so setting this is how a registered document gets a different ceiling from an ad-hoc one. 0 means no limit. Refused with 400 and DOCUMENT_. |
persisted_query. | integer | request. | The selection ceiling for a registered document, on the same terms. 0 means no limit. Refused with 400 and DOCUMENT_. |
What each preset sets:
| Preset | enabled | apq | only_ | audit_ |
|---|---|---|---|---|
off | false | false | false | false |
apq | true | true | false | false |
trusted_ | true | true | false | true |
trusted | true | false | true | false |
Two combinations are refused when the file is read, naming both keys:
| Refused | Why |
|---|---|
only_registered = true with apq = true | An allowlist any client can add to is not an allowlist. Set apq = false to enforce it. |
apq, only_ or audit_ set to true while enabled is false | A service that does not resolve hashes has nothing to register, refuse or audit, so the switches would read as configured and do nothing. |
[persisted_query]preset = "trusted_migrating"apq = false # collect the audit list without letting clients register[client]
Section titled “[client]”Which clients this service serves, and what it does with a request from one it does not recognize. The rules, including why a profile matched on a header is not a security boundary, are in telling your clients apart.
| Key | Type | Default | Does |
|---|---|---|---|
client. | "allow" | "reject" | "allow" | What happens to a request matching no profile. allow serves it with no client recorded against it. reject refuses it with 403 and UNKNOWN_, above the cache, before your origin is reached. A CORS preflight (an OPTIONS with Access- and no body) is never refused by reject, min_version, a profile’s limits or persisted_query = "required"; an OPTIONS with a body is checked as usual. |
[[client.profile]]
Section titled “[[client.profile]]”An ordered array. The first profile whose when is true wins. Every limit a profile sets can
only tighten the service-wide value of the same name; cache = "off" and max_age relax.
| Key | Type | Default | Does |
|---|---|---|---|
name | string | required | The canonical name, recorded on every request this profile matches. May not contain @ or be empty. |
when | expression | required | Whether this request is that client. Evaluated as a truth, so a bare header:x-app means “present and not empty”. |
version | expression | unset | Where to read the client’s version. Evaluated as a value. Absent means the client is recognized with no version recorded. |
min_version | string | unset | The oldest release of this client the service serves. Anything below it, or a client reporting no readable version, is refused with 403 and CLIENT_. A value that is not a valid version refuses nothing and logs CLIENT_. Without version, refuses the deployment. |
max_body | integer (bytes) | unset | A second ceiling under request.. A larger body is refused with 413 and PAYLOAD_, the same code the service-wide ceiling returns; your logs record CLIENT_ and name the client. |
max_ | integer | unset | How deep an operation from this client may nest. Refused with 400 and QUERY_. 0 refuses the deployment; leave the key out for no ceiling. |
max_ | integer (bytes) | unset | A second ceiling under request.. Refused with 400 and DOCUMENT_. 0 refuses the deployment. |
max_ | integer | unset | A second ceiling under request.. Refused with 400 and DOCUMENT_. 0 refuses the deployment. |
persisted_ | "any" | "required" | "any" | required refuses any operation from this client that does not arrive as a hash and nothing else, with 403 and PERSISTED_. A payload carrying a document is refused whatever hash travels beside it, so the APQ registration handshake is refused too. Does not apply to operations sent over a subscription socket. Ignored, logged as CLIENT_, while persisted_query. is false. |
cache | "on" | "off" | "on" | off takes this client off the shared cache: nothing is looked up, nothing is stored, every request reaches your origin. |
max_age | integer (seconds) | unset | How old a cached entry may be before this client stops accepting it. Over the cap, its request goes to the origin and reports PASS with CACHE_; the entry is unchanged for other clients. Together with cache = "off", refuses the deployment. |
[client]unknown = "allow"
[[client.profile]]name = "web-app"when = "header:gp-client-name == 'web-app'"version = "header:gp-client-version"min_version = "2.1.0"
[[client.profile]]name = "internal-bi"when = "auth:idp.role == 'analyst'"version = "auth:idp.client_version"cache = "off" # fresh data every time, straight from the originmax_query_depth = 6
[[client.profile]]name = "gp-cli"when = "starts_with(header:user-agent, 'gp-cli/')"version = "split(header:user-agent, '/', 1)"[telemetry]
Section titled “[telemetry]”What is recorded about a request.
| Key | Type | Default | Does |
|---|---|---|---|
telemetry. | integer | 32768 (32 KiB) | How much of the client’s query document is retained, on a request the edge answered without calling the origin itself (a cache hit, a request served through the shield). The document sent to the origin is not cut by this setting, only as far as the log line size limit requires. 0 removes the bound. Query hashes are always computed over the full text, so truncation never affects a cache key or a purge. |
telemetry. | string | "GP- | The request header a client sends its own correlation ID in. Matched case-insensitively. A name that is not a valid HTTP header name is refused at deploy. The header is forwarded to your origin unchanged, the default name included, unless [origin. removes it. See correlating logs. |
telemetry. | expression or boolean | unset (record everything) | Which requests are recorded in Insights: a policy expression read as a truth. "false" records nothing. See recording only some requests. |
telemetry. | "full", "truncated" or "none" | "full" | How much of the end user’s IP address, and of the geo and network data derived from it, the request’s telemetry keeps. Unless "full", no [telemetry. rule records gp-client-ip or a header that carries the client’s address either. Any other value refuses the deployment. See client IP address. |
telemetry. | "pseudonymized" or "clear" | "pseudonymized" | The form request telemetry stores the value of a [[cache. entry in, for entries whose pass_ is never or plain: a keyed pseudonym, or the value in clear. Any other value refuses the deployment. See vary values. |
Recording only some requests
Section titled “Recording only some requests”Every request a service serves is recorded in Insights, on every plan, unless telemetry.record
says otherwise. Set it to a predicate and only the requests it matches are recorded:
[telemetry]# Nothing at all.record = "false"[telemetry]# Everything but the cluster's health probes.record = "!starts_with(header:user-agent, 'kube-probe/')"- Unset records everything.
"false"records nothing, and a bare TOMLfalsemeans the same (record = trueis the same as leaving the key out). Any other value that is not a string, and an expression that does not parse, fails the deployment. - A request the expression cannot decide is recorded. The second example has no answer for a
caller that sent no User-Agent, and that caller is kept. To leave them out as well, write
header:user-agent && !starts_with(header:user-agent, 'kube-probe/'). See a request the expression cannot decide. - A request that is not recorded is still served and still counted. It is billed and counts
towards your allowance exactly like any other. Only what Insights stores changes: the request,
its log lines and any purge it triggers stay out of the request log, the charts, the purge
history and
gpilot requests. - It can only leave things out. Recording a request does not keep it while the account runs in lean mode.
- An expression over
auth:is decided on the verified claims. A request refused before authentication completes is decided without them. - The portal says so. With
"false", every Insights tab says the service records no requests and why, in place of empty lists; with a predicate, a note names the expression. The deployment also reportsrecord = "false"as an info finding.
There is no path or method source, so a health check is told apart by what it sends,
typically its User-Agent. The setting needs a runtime that ships it: an older runtime does not
read the key, records everything, and the deployment’s configuration report lists the key as
unknown.
Client IP address
Section titled “Client IP address”By default every recorded request keeps the client’s IP address as the edge saw it, plus the
geo and network data looked up from it. telemetry.client_ip reduces that per service, for
data-minimisation requirements, without turning telemetry off:
[telemetry]client_ip = "truncated""full"(the default) keeps the address and everything derived from it. Leaving the key out changes nothing."truncated"keeps the network part of the address: an IPv4 address with its last octet zeroed (203.0.113.77becomes203.0.113.0), an IPv6 address cut to its first 48 bits (2001:db8:85a3:8d3::7becomes2001:db8:85a3::). An IPv4 address in IPv6 notation (::ffff:203.0.113.77) is truncated as the IPv4 address it is. An address that cannot be read is not stored at all."none"stores no address.
| Field | full | truncated | none |
|---|---|---|---|
| Client IP | as seen | network part (/24 or /48) | not stored |
| Country, continent | kept | kept | kept |
| Region, city | kept | kept | not stored |
| Latitude, longitude, postal code | kept | not stored | not stored |
| UTC offset | kept | kept | not stored |
| ISP, ASN, connection speed and type, proxy type and description | kept | kept | not stored |
| Edge location and node, TLS and HTTP version | kept | kept | kept |
The reduction applies to the telemetry row. Log lines carry no client IP, country or city at
all, whatever the setting. The geo lookup itself still runs on the full address at
the edge; only what is stored is reduced, so the country of a "none" service is as accurate as
before.
- Counting is unaffected. Every request is still recorded, billed and counted towards your allowance exactly as before.
- Your origin is unaffected.
origin.forward_client_ipdecides whether your origin receives the full address ingp-client-ip, independently of this key. - Filtering by client IP is exact. On a
"truncated"service, filter by the truncated address (in Insights or withgpilot requests --ip=<address>). Only my requests and a baregpilot requests --ipuse your full address, so they match nothing on a"truncated"or"none"service. - In Insights, a truncated address is shown as stored (an IPv4 one in IPv6 notation, for
example
::ffff:203.0.113.0), and a request without one shows Not stored. With"none"the UTC offset reads0. - It applies from the next deployment on a runtime that ships the key. Requests recorded before keep what was stored then.
Vary values
Section titled “Vary values”Every request whose cache entry a [[cache.policy.vary]] entry partitioned
records the value that entry derived, shown in Insights as the request’s
segment. telemetry.vary_values decides the form it is stored in, per service, for entries whose
pass_to_origin is never (the default) or plain:
[telemetry]vary_values = "pseudonymized""pseudonymized"(the default) stores a keyed pseudonym,hmac-sha256:followed by 16 hexadecimal digits. It is made with a secret key of the service that neither you nor your origin holds, so the stored value cannot be turned back into the one it stands for. Equal values give equal pseudonyms within the service, so requests of one segment still group together and Requests in this bucket still finds them. Two services give the same value two different pseudonyms. A pseudonym is never replaced by the value itself: if it cannot be derived, nothing is recorded."clear"stores the value as derived. This is an explicit choice, and responsibility for it lies with you: a rule whose value is a session cookie, anAuthorizationheader, an email address or any other personal data then stores that data in your request telemetry, where Insights shows it. The[telemetry.headers]sanitize rules do not apply to it.
Not affected by this key:
- Entries with
pass_to_origin = "anonymized"store thev1=digest your origin receives, andcache.policy.privateandpublicalways store av1=digest. - The cache and your origin. What the cache partitions on and what
pass_to_originsends your origin stay the same; only the stored copy changes. - Counting. Every request is still recorded, billed and counted as before.
A value in clear never equals its pseudonym, so after a change of this key requests of one segment form two groups, before and after the deployment. Requests recorded before keep what was stored then, until your plan’s retention deletes them.
[telemetry.headers]
Section titled “[telemetry.headers]”Four independent rule lists. Each is an ordered list of rules; the first match wins, and a header no rule matches is not recorded.
| List | Covers |
|---|---|
telemetry. | The request as the client sent it. |
telemetry. | The request as it leaves for your origin, after vary scrubbing, your [origin. filter, the forwarding header strip and signing: what your origin actually received. |
telemetry. | The response as your origin sent it. The name is older than the fourth list; read it as “origin response”. |
telemetry. | The response as the client receives it, after [response. and cache.. The only list that shows GraphPilot’s own gp- headers, so it answers “what did my client actually see”. |
Some headers are never recorded, whatever a rule says, a catch-all .* included. Names are
matched case-insensitively.
- Always: every header whose name starts with
gp-vary-(the cache’s bucket discriminators), every header named in a[[cache.policy.vary]]entry, andgp-shield-hop. - Unless
telemetry.client_ipis"full"(so under"truncated"and"none"), the headers that carry the client’s address:gp-client-ip, every header whose name starts withx-forwarded-,forwarded,x-real-ip,x-client-ip,client-ip,true-client-ip,cf-connecting-ip,fastly-client-ip,x-original-url,x-rewrite-url,x-original-host,x-cluster-client-ip,cf-connecting-ipv6,x-envoy-external-addressandx-appengine-user-ip. Under"full"a rule that includes them records them like any other header; no default rule does. The last four are not among the forwarding headers thatorigin.forwarded_headersstrips.
A rule names either an exact header or a pattern, plus an action:
| Action | Effect |
|---|---|
include | Record the header and its value. The default when a rule names no action. |
sanitize | Record a keyed digest of the secret part instead of the value: hmac-sha256: followed by 16 hex digits, an HMAC-SHA-256 with a secret key held per service. The parts that carry no user data stay readable: a cookie’s name, an authorization scheme such as Bearer, and Set-Cookie attributes such as Secure or SameSite=Lax. The header is still visible as present, and two requests of one service carrying the same value get the same digest, so they are still comparable. The same value in two services gives two different digests, and a digest cannot be recomputed from candidate values without the key. If the service’s key cannot be read, each secret part is recorded as [redacted], never as a plain hash. |
exclude | Do not record it at all. |
[[telemetry.headers.request]]name = "authorization"action = "sanitize"
[[telemetry.headers.request]]pattern = "^x-internal-"action = "exclude"
[[telemetry.headers.request]]pattern = ".*"action = "include"The defaults are deliberately narrow. Unless you replace a list, these are the only headers recorded:
| List | Sanitized | Recorded |
|---|---|---|
request | authorization, cookie | content-type, accept, referer |
origin_ | authorization, cookie, gp-signature | gp-timestamp, content-type, accept |
response | set-cookie | content-type, cache-, server, content- |
client_ | set-cookie | every gp- header except gp- (excluded, the telemetry row carries the keys already), plus cache-, age, content-type, content- and vary |
[surrogate_key] and [cache_control]
Section titled “[surrogate_key] and [cache_control]”How GraphPilot reads your schema’s directives, and which fields count as keys without any directive at all.
[[surrogate_key.directives]] and [[cache_control.directives]]
Section titled “[[surrogate_key.directives]] and [[cache_control.directives]]”Repeatable. Each entry names a directive GraphPilot reads, and the arguments it reads from it. See renaming directives.
The built-in entry uses the values in the second column. In an entry you write, a key marked
required must be set; an optional surrogate_key argument you leave out is unset, and an
optional cache_control key you leave out takes the built-in value.
| Key | Built-in value | In an entry you write |
|---|---|---|
surrogate_key. | "surrogateKey" | Required. The directive, without the @. |
surrogate_key. | "of" | Optional. Unset when left out: the directive reads no key fields, so it works only as a marker. |
surrogate_key. | "static" | Optional. Unset when left out: the directive reads no fixed keys. |
surrogate_key. | "ignore" | Optional. Unset when left out: ignore: true on that directive does nothing, and no diagnostic says so. |
cache_control. | "cacheControl" | Required. The directive, without the @. |
cache_control. | "maxAge" | Required. The lifetime argument. |
cache_control. | "scope" | Required. The scope argument. |
cache_control. | "inheritMaxAge" | Required. The inheritance argument. |
cache_control. | "swr" | Optional; "swr" when left out. The stale-while-revalidate argument. |
cache_control. | "vary" | Optional; "vary" when left out. The directive that declares a named scope. |
cache_control. | "names" | Optional; "names" when left out. That directive’s list argument. |
[[surrogate_key.directives]]directive_name = "key" # read Apollo Federation's @key(fields:)of_arg = "fields"An entry replaces the built-in directive (see the caution below). directives = [] switches the
family off, so no directive is recognized.
Automatic key fields
Section titled “Automatic key fields”| Key | Type | Default | Does |
|---|---|---|---|
surrogate_key. | list of strings | ["ID"] | Scalar type names whose fields are treated as key fields. |
surrogate_key. | list of strings | ["id", "_id", "key"] | Field names treated as key fields whatever their type. |
surrogate_key. | list of strings | [] | Object types and interfaces the two rules above do not key. An interface listed here does not take the types implementing it along. |
surrogate_key. | list of strings, each "Type.field" | [] | Single fields of an object type or interface the two rules above do not key. |
surrogate_key. | integer | 1 | How many levels below a mutation’s return type the deployment build looks for types that carry surrogate keys, so a mutation purges an entity the client did not select. 1 reaches the entity inside a Relay-style payload (updateX: UpdateXPayload { x: X }); 0 switches this off. 2 also reaches the entity’s owner and purges every cached response mentioning it, which is broad: prefer an owner key on the entity’s type, such as @surrogateKey(of: "service.id"). See automatic invalidation. |
[surrogate_key]ignored_types = ["RenderedSnippet"]ignored_fields = ["Product.key"]The exclusions switch off only those two automatic rules, never a key your schema declares; the
build reports the conflict as a warning. The schema makes the same exclusions with
@surrogateKey(ignore: true). An entry that matches no object type,
interface or field is a warning, and an ignored_fields entry not of the form Type.field is an
error; both exclude nothing and neither fails the deployment.
[runtime]
Section titled “[runtime]”| Key | Type | Default | Does |
|---|---|---|---|
runtime. | string | none | The proxy runtime version for the one deployment that gpilot deployment create makes from this file. See Runtime versions. |
This key pins only that deployment. The CLI checks the version first and refuses an unknown or
withdrawn one before uploading anything, removes [runtime] version from the uploaded config
and sends it as the deployment’s runtime version. It is not the service’s pin: a redeploy from
the portal, or a change of the pin, does not carry it. Set the service-level pin in the portal
or with gpilot service runtime pin <version>. Changing it redeploys the service unless the
service already runs the version the pin resolves to, or you save it for the next deployment in
the portal; either way it needs service:edit and deployment:edit.
Settings that do nothing
Section titled “Settings that do nothing”Stated so nobody configures them expecting an effect:
- The
[deployment]section andruntime.serving_modeare written by GraphPilot. A value you supply is ignored.
Validation
Section titled “Validation”Unknown keys
Section titled “Unknown keys”Whether a key GraphPilot does not know is refused depends on the section it is written in.
Refused. These sections deny unknown keys, so a misspelling in them fails when the file is read and the deployment does not start:
[client]and every[[client.profile]][persisted_query][response],[response.headers],[response.cookies], and every entry in theirrules[response.cors][origin.headers],[origin.cookies]and[origin.retry][cache.request_directives],[cache.http_key],[cache.headers]and[cache.cookies][auth.rejection]
Ignored. Everywhere else an unknown key deploys cleanly and does nothing: the top level of
the file, [request], [origin], [cache], [cache.policy], every [[cache.policy.vary]]
entry, [auth], every [auth.providers.<name>] block, [telemetry], [telemetry.headers],
[surrogate_key] and [cache_control], and [runtime] apart from its version key. So a typo like [cache.polcy] or [reqest], or a
misspelled key inside a provider block, is silent at deploy time.
Other refusals
Section titled “Other refusals”- An invalid enum value fails when the configuration is read. Values are matched exactly as spelled here; there is no case folding.
- An unparseable policy expression fails, and the whole expression must parse: a typo does not become a shorter, different expression.
- A
[[cache.policy.vary]]entry whosenameis not a legal header name is the exception to that: it does not fail, it is skipped, so the rule never fires. The deployment records it as an error in its diagnostics, which the portal shows under findings andgpilot deployprints when the deployment finishes. - An
api_keyprovider with an emptyprefixeslist, or one containing an empty string, is refused with a message saying why. - A duplicate provider name is rejected by TOML itself, before any of this runs.
- A provider name longer than 977 bytes (UTF-8) refuses the deployment with
AUTH_PROVIDER_NAME_TOO_LONG, before a deployment is created. Every[auth.providers.<name>]block counts, with or without ajwks_url. - An origin timeout above your plan’s ceiling or below
100ms, or a retry policy whose worst case does not fit 120 seconds, refuses the deployment. See[origin]. - A persisted query combination that contradicts itself is refused. See
[persisted_query].