> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# The developer GraphQL shaped-read model

> Why Devotel Orbit's read-only GraphQL surface exists alongside REST, what its deliberately-small query grammar covers and refuses, how the served SDL is pinned to the executed schema, how query complexity is capped, how its result envelope differs from REST, and the tenant isolation every query inherits

# The developer GraphQL shaped-read model

REST endpoints on Orbit return one resource per response. When a read is
shaped like a tree — a contact with its conversations, and each
conversation's messages — the per-resource pattern becomes a chain of
requests: N+1 round-trips and a client-side join. The developer GraphQL
surface is the answer to that shape: you describe the tree, one request
returns it nested.

This page is the model for that surface: what it is, what it refuses, and
which invariants every query rides on. For endpoint-by-endpoint
walkthroughs and copy-pasteable queries in seven SDK languages, use the
[GraphQL querying guide](/guides/graphql-querying) — this page explains
the design, that guide shows the calls.

## Why a shaped-read surface exists alongside REST

Orbit's machine-readable surface already speaks four contracts for the
per-resource world: the REST API and its OpenAPI document, the AsyncAPI
contract for events, and the Postman collection that regenerates from
OpenAPI. Those contracts answer "give me resource X" — precisely, one
endpoint at a time.

A shaped read is the case where none of them fit: the response you want
is a contact joined to two other collections, and building it from
per-resource endpoints means three sequential requests plus glue code
that re-implements a database join in your application. The GraphQL
surface covers exactly one such tree — the four highest-traffic CPaaS/CDP
resources, contacts, conversations, messages, and segments — so
resolution-shaped reads become one request. Everything else, and every
write, still belongs to REST: mutations are not part of the surface at
all, which is what keeps it small enough to hold invariants over.

## A deliberately minimal grammar

The surface accepts a strict subset of the GraphQL language. Supporting
the subset — rather than shipping a general GraphQL server — is the
point: every rejected feature below is a behavior class the surface can
then promise never to have.

**Supported:**

* One `query` operation per document, named or anonymous shorthand
* Variable definitions with `$var` references and default values
* Field arguments over scalars, lists, objects, enums, and null
* Nested selection sets, field aliases, and the `__typename` meta-field

**Not supported, by design:**

* **Mutations and subscriptions** — the surface is read-only; writes go
  through the REST endpoints
* **Fragments** (`fragment`, `...spread`) — the parser rejects the
  keyword outright
* **Directives** (`@include`, `@skip`, custom) — not part of the grammar
* **`__schema` / `__type` introspection** — only `__typename` exists;
  the schema travels as a served SDL document instead

## The served SDL is the executed schema

Introspection is absent because the schema arrives out of band:
`GET /api/v1/developer/graphql/schema` returns the full SDL document as
plain text, served alongside the OpenAPI, AsyncAPI, and Postman
contracts.

The SDL and the executor render from the same source. One schema
descriptor drives both paths: execution validates every field, argument,
and selection against it, and the SDL endpoint prints it. Documentation
can never drift from behavior, because there is only one descriptor.
That makes **pin to the SDL** the integration contract: a field listed
in the served SDL with arguments it declares will execute; anything not
listed fails validation instead of silently returning null. Updates to
the surface land in both places at once, and the served document is the
diff you integrate against.

## Complexity accounting

Depth limits alone do not bound what a query will cost. A selection only
three levels deep — 100 contacts, each with 50 conversations, each with
100 messages — fans out to thousands of per-parent resolver calls, and
aliases let a query repeat that subtree arbitrarily many times. Left
unbounded, that is a cross-tenant denial-of-service vector.

So every query pays for what it asks before it runs. The engine
statically estimates resolver-backed work: each object-typed field costs
one unit per parent that resolves it, a list field multiplies everything
beneath it by its page size (`limit`, clamped to the field's maximum),
and scalar leaves cost nothing because they read from an already-fetched
row. Aliased repeats count fully. When the estimate exceeds 1000 units
the request is rejected in validation with `Query is too complex` —
before any resolver executes. Nesting depth separately caps at eight
levels.

## The result envelope, and where it stops

Query-level failures return HTTP 200 with a `{ data, errors }` body:
each failing field comes back `null` at its path with a corresponding
entry in `errors`, so partial results survive one broken subtree.
Validation verdicts — an unknown field, a missing sub-selection, the
complexity rejection, a parse failure on an otherwise well-formed
request — land in the same envelope.

That convention stops at the query boundary. Transport failures still
answer as ordinary HTTP errors: authentication and role checks reject
with 401/403, malformed bodies with 422, and rate limiting with 429.
Result-envelope errors never mask those, and you should treat both
channels in client code: HTTP status first, then `errors` inside a 200.

## Safety invariants, inherited from the developer routes

The surface holds the same guarantees as every other developer route,
because it sits behind the same guards:

* **Authentication and role** — an API key or dashboard session with an
  `owner`, `admin`, or `developer` role, exactly as the REST developer
  routes require
* **Tenant isolation** — resolvers close over one tenant-schema
  reference derived from the caller's context and validated as a safe
  identifier; every statement is parameterized. A query can only ever
  read the calling organization's own data
* **Read-only** — no write of any kind, and the surface touches no
  outbound sending path at all: it reads contacts, conversations,
  messages, and segments

## This page vs the guide

Concepts owns the model; the guide owns the mechanics. Read this page to
understand what the surface is and what it promises — then do the work
in the [GraphQL querying guide](/guides/graphql-querying): the endpoint
contract, per-language request examples across the SDKs, and worked
queries. For per-resource operations and all writes, the
[API reference](/api-reference) remains the contract; the SDL endpoint
above is the GraphQL schema's public form.
