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 slugall<Type>— paginated list with filterscount<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.