Skip to Content
API ReferenceAuthorization

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:

  1. Authentication — the x-api-key must be a valid, active key. A missing or bad key is UNAUTHENTICATED.
  2. Project binding — a project key serves its own project; a tenant key resolves the target from the X-Project-Id header against its allow-list. A missing or off-list project is rejected before any data is touched.
  3. Scope — the operation must be permitted by the key’s scopes (and key polarity: pk_ is read-only, sk_ may write). A missing scope is missing required scope; a public key attempting a write is PERMISSION_DENIED.
  4. 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

CredentialReaches
pk_ key alonePublished records whose visibility is public. Read-only. Safe in a browser.
pk_ key + customer JWTThe 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:readThe 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 seeNOT_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

ConditionHTTPextensions.codeMessage
Missing / invalid / disabled key401UNAUTHENTICATEDAuthentication required
Public key attempting a write; domain-restriction mismatch; customer JWT invalid for a customer resource403PERMISSION_DENIEDInsufficient permissions
Key valid but lacks a required scope403(none — see below)missing required scope: <scope>
Tenant key with no X-Project-Id400X-Project-Id is required for a tenant API key
Tenant key, project not on allow-list403X-Project-Id is not on the key's project allow-list
Targeted read of a row you can’t see (incl. unpublished without preview)200NOT_FOUNDRecord not found

Watch the scope-denial case. A plain scope denial (missing required scope: <scope>) is returned without an extensions.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:

  1. What key? pk_ → published + public only. sk_ → operator view within scopes.
  2. Customer token present? If so, that customer’s owned/granted rows are added on top.
  3. What scopes? The operation must be in them, or it’s a call-level rejection.
  4. 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

Last updated on