Production Board

GraphQL API

Optional: query and mutate the same data via a single GraphQL endpoint.

GraphQL is optional. The HTTP API and the WebSocket channel already cover every operation - this page is here for services that prefer a single endpoint with a typed query language. POST a query to /xapi2/graphql with the same Bearer token the rest of the API accepts and you get the same data back, in a GraphQL response shape. Behind the wire it is the same infrastructure the HTTP and WebSocket paths run on - the GraphQL endpoint is a thin layer on top, not a parallel system. Authentication, personal-access-token scopes, per-IP and per-token rate limits, row-level access checks, audit logging, validation rules, plan quotas, and live event fan-out to subscribed WebSocket clients are all the *exact same code paths* the REST endpoints exercise. A bug fix or a new access rule lands across all three at once. The schema itself is intentionally compact: one Object type whose payload travels as a JSON scalar, plus six roots (object, objects, createObject, updateObject, deleteObject). Field definitions per model live on the matching model page, which stays the source of truth for what each row carries.

How it works

Read

object(type, id) returns one row, objects(type, limit, offset, sort, q, filters, withCount) returns a paginated list. Both honour the same row-level access rules as the HTTP GET paths - rows you can read over REST you can read here, rows you can't see over REST you can't see here.

Mutate

createObject(type, input), updateObject(type, id, input), and deleteObject(type, id) mirror the HTTP POST / PATCH / DELETE routes. Every write runs the same validation rules and plan checks, fans out to the same live WebSocket subscribers, and lands in the same audit log the REST endpoints feed.

Same security envelope

Authentication, scopes, abuse protection, and rate limits behave identically to the HTTP API. There is no "GraphQL bypass" - every root field charges its own rate-limit token and runs through the same access checks a REST call would, so a single document carrying many operations costs the same as the equivalent series of REST calls.

Document limits

A single document is capped at 64 KiB, depth 8, 200 total field selections, 10 root fields per operation, and 50 variables. These bounds keep one query's worst-case cost predictable; comfortably above any realistic dashboard composition.

Security

The GraphQL endpoint runs on the same infrastructure as the HTTP API and the WebSocket channel - it is a thin layer on top, not a parallel system. Authentication, PAT scopes, rate limits, row-level access checks, audit logging, and live event delivery to WebSocket clients all flow through the *exact same code paths*. Anything you can't do over REST you can't do over GraphQL - there is no "GraphQL backdoor." For per-model rules, see the matching model page.

Schema

The schema is intentionally small - one Object shape with a JSON payload plus six root fields. Per-model field shapes live on the matching model page.

scalar JSON
type Object {
id: ID!
type: String!
status: String
ownedBy: ID
createdBy: ID
updatedBy: ID
appSlug: String
data: JSON
createdAt: String
updatedAt: String
isArchived: Boolean
}
type ObjectList {
data: [Object!]!
count: Int!
hasMore: Boolean!
limit: Int
offset: Int
sort: String
appSlug: String
total: Int
}
type DeleteResult { ok: Boolean!, id: ID! }
type Query {
object (type: String!, id: ID!): Object
objects (
type: String!,
limit: Int, offset: Int, after: ID,
sort: String, q: String,
withCount: Boolean, filters: JSON,
): ObjectList!
}
type Mutation {
createObject (type: String!, input: JSON!): Object!
updateObject (type: String!, id: ID!, input: JSON!): Object!
deleteObject (type: String!, id: ID!): DeleteResult!
}

Examples

curl -X POST https://qtssystem.com/xapi2/graphql \
-H "Authorization: Bearer pat_…" \
-H "Content-Type: application/json" \
-d '{
"query": "query($t:String!,$id:ID!){ object(type:$t,id:$id){ id data } }",
"variables": { "t": "board", "id": "<object-id>" }
}'

Limits

A single document is bounded by the values below - comfortably above any realistic dashboard composition. These caps keep one query's worst-case cost predictable.

Max query size64 KiB
Max depth8
Max selections (total)200
Max root fields per op10
Max variables50
Rate limit per root field1 token (same pool as REST)

Deliberately unsupported

  • Subscriptions

    Live updates ride the WebSocket channel, which enforces the same access layer. A second live surface over GraphQL would just double the security footprint with no new capability.

  • Fragments

    With the compact schema (one `Object` shape, six roots) fragments add no reuse - just extra parser and validation surface. Inline selections cover every realistic shape and are simpler to audit.

  • Directives

    Conditional selections (`@skip` / `@include`) are simpler and safer expressed as a client-side branch when building the query. The server refuses directives so a selection set always reads literally.

  • Introspection

    The full schema is on this page (and on the per-model pages) - a server endpoint that re-publishes the schema is unnecessary, and shipping one would be a reconnaissance surface for attackers.