Authorization
Authentication proves who a request is. Authorization decides what that request can reach. This page is the request-level reference: given a credential and a query, what does the API return, and why. For the conceptual model behind it — tenancy, owner planes, visibility, and grants — see Authorization & Access.
Two things carry authorization on every request:
- the API key, which binds the request to a project (or an allow-listed set of projects) and carries the scopes that bound what it may do;
- an optional customer JWT, which identifies the signed-in end user, so customer-owned data resolves to that person.
The key answers may this request perform this operation at all. The database answers which rows it may touch. Both must pass.
How a request resolves
Every request is checked in this order. The first failing gate ends the request:
- Authentication — the
x-api-keymust be a valid, active key. A missing or bad key isUNAUTHENTICATED. - Project binding — a project key serves its own project; a tenant key resolves the target from the
X-Project-Idheader against its allow-list. A missing or off-list project is rejected before any data is touched. - Scope — the operation must be permitted by the key’s scopes (and key polarity:
pk_is read-only,sk_may write). A missing scope ismissing required scope; a public key attempting a write isPERMISSION_DENIED. - Row rules — for the rows the query actually touches, the database applies owner plane, visibility, the customer identity from the JWT, and any grants. Rows you can’t see are simply not returned, and a targeted read of one resolves as
NOT_FOUND.
Gates 1–3 are about the call; gate 4 is about the rows. A key can be fully authorized to run a query and still legitimately get back fewer rows than exist — that’s gate 4 doing its job, not an error.
What each credential can reach
| Credential | Reaches |
|---|---|
pk_ key alone | Published records whose visibility is public. Read-only. Safe in a browser. |
pk_ key + customer JWT | The above, plus the authenticated customer’s own records (currentUser, my<Model>) and anything explicitly granted to them. Customer self-service queries need no api-key scope — a valid key plus the customer token is enough. |
sk_ key (server-side) | Full read/write within the key’s scopes, across every owner’s records that the scopes permit. Used for operator/back-office reads and writes, never shipped to a browser. |
sk_ key + drafts:read | The above, plus unpublished draft content via preview: true. |
The pattern to internalize: a public key is the anonymous, published, public view; a customer JWT adds that customer’s private data on top; a secret key is the server-side operator view. You widen access by presenting a stronger credential, never by changing the query.
# pk_ alone → published, public only
query PublicCatalog {
products(first: 20) { edges { node { _id title } } }
}# pk_ + customer Bearer token → the same catalog, plus this customer's orders,
# resolved to them by the database, not by a filter in the query.
query MyAccount {
currentUser { id email }
orders(first: 20) { edges { node { _id total status } } }
}Scopes gate the call, not the rows
A scope decides whether a request may perform an operation; it never widens which rows that operation returns. A sk_ key with records:read can read a customer-owned model — and gets back every owner’s rows the row rules allow for a secret operator credential — while the same read made with a pk_ key and a customer token returns only that customer’s rows. Same query, same model; the credential changes what the row layer resolves.
This is why you can hand out records:read broadly without it becoming a data-exposure decision: the scope opens the door to the model, and ownership plus visibility still decide what’s on the other side. Per-model scopes (records:read:<model>, records:write:<model>, …) narrow the door further for tokens that should only reach specific models — see scopes.
Writes
Writes require a secret key (sk_) with the matching write scope — records:write, records:delete, records:publish. A pk_ key is rejected at the scope gate; it can never hold a write scope. Beyond the scope, the row rules still apply: a write only lands on rows the credential is allowed to modify.
Customers write their own data without any api-key write scope, through the generated self-service mutations (setMy<Model>), because ownership makes the target unambiguous — a customer’s write can only ever address their own record. See Customer-owned data.
Preview and drafts
By default reads return published content. Pass preview: true to read the latest working version instead. Preview is gated by the drafts:read scope, which is grantable only to sk_ keys — a pk_ key is rejected at key-creation time, so draft content can never leak through a browser-embeddable key.
A preview: true read made without drafts:read fails loudly with an authorization error rather than silently returning published data, so a misconfigured preview deploy breaks visibly instead of serving stale content. Preview responses also come back Cache-Control: no-store. See Preview Mode Access.
Rows you can’t see are NOT_FOUND
When a targeted read (by id or natural key) names a record your credential can’t see, the API returns NOT_FOUND — the same response as a record that genuinely doesn’t exist. The API deliberately does not distinguish “doesn’t exist” from “exists but not yours,” so existence itself isn’t leaked to a caller who lacks access. The documented case is a record that is unpublished and read without preview: true: it reports NOT_FOUND, not a permission error.
Contrast this with the call-level failures (UNAUTHENTICATED, PERMISSION_DENIED, missing required scope), which are about the key and are reported plainly because they carry no information about any specific row. The rule of thumb:
- Wrong or under-scoped key → a permission/auth error. Fix the credential.
- Right key, row you can’t see →
NOT_FOUND. The credential is fine; that row isn’t yours.
Caching honors the credential
The response cache never serves one caller’s private data to another. Entries computed for a customer are keyed PRIVATE, which folds the customer id into the cache key, so each customer gets isolated entries; only public, published responses are cached PUBLIC and shared across callers whose query, variables, schema channel, and scopes match. Authorization and caching can’t disagree, because the cache scope is derived from what the request was allowed to see.
Authorization error reference
| Condition | HTTP | extensions.code | Message |
|---|---|---|---|
| Missing / invalid / disabled key | 401 | UNAUTHENTICATED | Authentication required |
| Public key attempting a write; domain-restriction mismatch; customer JWT invalid for a customer resource | 403 | PERMISSION_DENIED | Insufficient permissions |
| Key valid but lacks a required scope | 403 | (none — see below) | missing required scope: <scope> |
Tenant key with no X-Project-Id | 400 | — | X-Project-Id is required for a tenant API key |
| Tenant key, project not on allow-list | 403 | — | X-Project-Id is not on the key's project allow-list |
Targeted read of a row you can’t see (incl. unpublished without preview) | 200 | NOT_FOUND | Record not found |
Watch the scope-denial case. A plain scope denial (
missing required scope: <scope>) is returned without anextensions.code. Branch on the message for that case rather than on a code — every other authorization failure carries a code.
Reasoning about a query
To predict what a request returns, read it credential-first, not query-first:
- What key?
pk_→ published +publiconly.sk_→ operator view within scopes. - Customer token present? If so, that customer’s owned/granted rows are added on top.
- What scopes? The operation must be in them, or it’s a call-level rejection.
- What owner plane and visibility do the touched models have? That, plus the identity above, decides the rows.
If a query returns fewer rows than you expected, it’s almost always step 4 — the credential can’t see them — not a bug in the query. Present a stronger credential (a customer token, or a secret key for operator reads); don’t try to widen the query.
See also
- Authorization & Access — the conceptual model (tenancy, owner planes, visibility, grants)
- Authentication — key types, scopes, tenant keys, customer JWTs
- Errors & Rate Limits — full error-code reference
- Response Caching —
PUBLICvsPRIVATEcache scope - Customers — customer-owned models and self-service queries