GraphQL

GraphQL is the primary API surface. The schema is generated from your content types via Pothos; introspection drives @estokad/sdk for end-to-end type safety.

Endpoint

POST https://api.estokad.com/v1/<workspace>/graphql

A GraphiQL playground is exposed at the same path on a GET request when the read_draft or management scope is present.

Query shape

Every content type produces three fields on the root query:

  • <type> — single entry by id or slug
  • all<Type> — paginated list with filters
  • count<Type> — total count for the same filters

Singletons get one field instead: <type>.

query {
  allArticle(first: 10, where: { publishedAt: { gte: "2026-01-01" } }) {
    edges {
      node {
        id
        title
        slug
        publishedAt
        author {
          name
          avatar { url }
        }
      }
    }
    pageInfo { hasNextPage endCursor }
  }
}

Filters

Each scalar field gets a filter input with the comparators that make sense for its type — eq, neq, in, lt, lte, gt, gte for numbers and dates; eq, neq, in, contains, startsWith for strings. Boolean fields get just eq.

References use nested filters:

query {
  allArticle(where: { author: { name: { contains: "Marie" } } }) {
    edges { node { id title } }
  }
}

Pagination

Cursor-based, Relay-spec compliant. first + after for forward; last + before for backward. Page sizes are bounded at 100 — larger requests get clipped silently and the next cursor lets you continue.

Draft mode

Send X-Estokad-Draft: 1 to read drafts in addition to published entries. The header is honoured only by read_draft and management-scope keys; lower scopes get a 403.

The typed SDK

@estokad/sdk generates per-type method names from your schema and wraps them in a typed client. The example above through the SDK:

import { createClient } from '@estokad/sdk'

const cms = createClient({
  workspace: 'your-workspace-slug',
  apiKey: process.env.ESTOKAD_PUBLIC_KEY,
})

const result = await cms.article.list({
  first: 10,
  where: { publishedAt: { gte: '2026-01-01' } },
  fields: { id: true, title: true, slug: true, publishedAt: true },
})

Method names, argument types, and return types are all derived from your schema. Renaming a field in your schema produces a TypeScript error in your SDK call sites until you update them — exactly the safety net you'd expect.

Mutations

Writes go through REST. Mutations on the GraphQL surface are deliberately not exposed — keeping mutations REST keeps the GraphQL surface read-only, simpler to cache, and easier to expose to public clients.