Skip to content

GraphQL API

Peacock exposes the public CDN read-surface as a read-only GraphQL endpoint. Same content as /v1/cdn/{space}/stories/{path}, but you get to query exactly the fields you need.

Endpoint

text
POST /v1/cdn/{space}/graphql
Content-Type: application/json

Anonymous (no token). Rate-limited at throttle:600,1 (600 requests per minute per IP — same bucket as the REST CDN endpoint).

Why read-only

Mutations stay on the REST surface where the existing auth + rate-limit + audit infrastructure lives. GraphQL exists to give SDK consumers query-shape control over multiple round-trips, not as an alternative write path.

Quick example

bash
curl -X POST https://peacock-cms.webhoch.com/v1/cdn/demo/graphql \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "query($path: String!) { cdnStory(spaceSlug: \"demo\", path: $path) { uuid slug lang content } }",
    "variables": { "path": "/welcome" }
  }'

Response:

json
{
  "data": {
    "cdnStory": {
      "uuid": "492f1762-cfbd-496f-869f-006bcf9adacd",
      "slug": "welcome",
      "lang": "en",
      "content": "{\"type\":\"doc\",\"blocks\":[...]}"
    }
  }
}

content is JSON-stringified — parse it client-side with JSON.parse(...). The same blocks tree that the REST /stories/{path} endpoint returns.

Schema

The schema is intentionally tiny — one root query, one Story type.

graphql
type Query {
  "Fetch the currently-published Story at a path in a Space."
  cdnStory(spaceSlug: String!, path: String!, lang: String): Story
}

"A published Story served from the public CDN."
type Story {
  uuid: String!
  slug: String!
  fullPath: String!
  lang: String!
  status: String!
  content: String!   # JSON-stringified blocks tree
  updatedAt: String!
}

Static schema URL (for tooling)

Runtime introspection (__schema, __type) is disabled — see Sicherheit below. To plug Peacock into editor plugins, graphql-codegen, Apollo CLI, or any tool that needs the SDL, point it at the static schema endpoint:

bash
curl https://peacock-cms.webhoch.com/v1/graphql/schema.gql

Returns text/plain SDL. Cached for 24h at the edge; regenerated on every deploy.

yaml
# graphql-config example
schema: https://peacock-cms.webhoch.com/v1/graphql/schema.gql
documents: 'src/**/*.graphql'
ts
// @graphql-codegen/cli example
generates:
  src/types/peacock.ts:
    schema: https://peacock-cms.webhoch.com/v1/graphql/schema.gql
    plugins:
      - typescript
      - typescript-operations

Locale resolution

The lang arg follows the same BCP-47 fallback chain as the REST endpoint. Missing translations fall back to the space-default locale — never a hard 404 for an existing path. The chosen locale is reflected in meta.served_lang when you query cdnStory { ... } via the REST endpoint; in GraphQL today, the Story.lang field carries it.

Limits

LimitValueWhy
Query length8 000 charsDoS protection; legitimate queries never come close
Rate limit600/min per IPSame as REST CDN; tracked per-IP not per-token
Field selectionStory scalar fields onlyNo nested-resolver fan-out — single SELECT per request
Mutations❌ disabledReads-only; use REST /v1/spaces/.../stories/... for writes
Introspection❌ disabledSee below

Sicherheit

Introspection is OFF

Security audit 2026-05-21 (Medium #3) — runtime introspection lets attackers enumerate the full schema, which on a typical Lighthouse-PHP deployment also leaks resolver class names, DB column hints, etc. Peacock disables __schema + __type at query-validation time, and strips internal exception messages from the error response so DB paths, file names, and stack traces never reach the client.

VectorSchutz
Schema enumeration via __schemaBlocked at validation rule — query rejected before resolver runs
Exception-message leakreport($e); return ['errors' => ['message' => 'execution failed']] — original exception is server-logged only
Octane state leakDisableIntrospection rule is per-execution (passed via $validationRules), not via the library's process-global DocumentValidator::addRule() — verified by code-reviewer agent
DoS via deeply-nested queriesSchema has no nested object types; cannot exceed depth 2

Why not Lighthouse-PHP

Lighthouse is excellent when you have ~20+ entities and need directive-driven schema management. For Peacock's single-entity public read surface, a hand-rolled webonyx/graphql-php schema is ~50 lines and adds zero dependency weight to the framework's auto-discovery. When the Management API gets a GraphQL surface (post-Phase-9), Lighthouse becomes the right tool.

See also