Skip to content

GraphQL subscriptions

The GraphQL transport serves subscriptions over graphql-transport-ws on /graphql. It carries subscriptions only, not the item CRUD operations that the item protocol offers. It is configured by the WEBSOCKETS_GRAPHQL_* family (see Manage configuration).

A GraphQL subscription receives no initial snapshot. It receives the change events only, in the shape of the selection set.

Open a WebSocket to /graphql with the graphql-transport-ws subprotocol, then send connection_init. Under handshake or public, the access token travels in the init payload:

{ "type": "connection_init", "payload": { "access_token": "<token>" } }

Under strict, present the token as an Authorization: Bearer header at the upgrade and send connection_init with no token in the payload. The server answers with connection_ack.

Each subscribable collection exposes a <collection>_mutated field on the Subscription root. It takes an optional event argument (create, update, or delete) and returns the changed record.

subscription {
articles_mutated(event: create) {
event
data {
id
title
}
}
}

The selection set chooses the fields to receive. event is the change kind. data is the changed item, selected field by field. Omit the event argument to receive create and update.

Standard GraphQL aliases and fragments work, and multiple _mutated subscriptions can run on one connection, each as its own operation.

A delete is delivered only when event: delete is requested. Because a deleted item cannot be read back, the delete result carries the key and a null data:

subscription {
articles_mutated(event: delete) {
event
key
data {
id
}
}
}

A delivered delete has the exact shape { "event": "delete", "key": "<id>", "data": null }. The key is the deleted primary key as a string. The delete feed has eligibility rules described under the delete feed. An ineligible delete subscription returns a FORBIDDEN error.

The GraphQL subscription surface is scoped to items, so a <collection>_mutated field exists for item collections only. The dashboards, notifications, operations, panels, and shares scopes that the item protocol can observe are not exposed here. Use the item protocol for those.

The GraphQL WebSocket surface enforces the same guards as the GraphQL HTTP endpoint:

  • Query token limit. A subscription document larger than GRAPHQL_QUERY_TOKEN_LIMIT is rejected.
  • Introspection. When GRAPHQL_INTROSPECTION is off, an introspecting document is rejected.
  • Error redaction. Internal error detail is redacted from the error sent to the client.

GraphQL has no in-band reauthentication. In public mode, expiry drops the connection to anonymous access without a notice frame. Reconnect with a fresh token and resubscribe to restore authenticated access. In handshake and strict, expiry closes the connection and requires a reconnect. See the authentication table for the full mode-specific outcomes.

The GraphQL transport uses the graphql-transport-ws close codes. The ones to expect:

  • 4400 — a malformed frame, or a frame that is not valid for the protocol state.
  • 4401 — a subscribe was sent before the connection was acknowledged.
  • 4403 — forbidden: an authentication failure at connection_init, or a token supplied under strict.
  • 4408 — connection-initialisation timeout: no connection_init arrived within the deadline. This applies under public and strict. Under handshake the graphql-transport-ws init timer is disabled and the base authentication deadline applies instead, so a handshake connection that never authenticates closes on that deadline without this protocol code.
  • 4409 — a subscribe reused an operation id that is already active on the connection.
  • 4429 — more than one connection_init was sent.
  • 1009 — an outbound frame would exceed the 1 MiB frame bound.
  • 1013 — try again later, when capacity, a rate limit, or the pending-command limit is exceeded.
  • 4500 — an internal error while delivering a subscription.

The 4400, 4403, 1013, and 4500 closes come from CairnCMS. The 4401, 4408, 4409, and 4429 closes come from the graphql-transport-ws protocol layer, and 1009 from the shared outbound-frame guard.

On any close, reconnect and resubscribe, then reread current state. A subscription is ended cleanly with the protocol’s complete message.