Skip to main content

Running the API

This documentation site is powered by GraphQL. Every page you are reading — including this one — is loaded into a small GraphQL server that ships inside the keen-public-docs repo, so the docs do not just describe a GraphQL API, they are one. You can query the doc tree, filter by section, and run full-text search programmatically, with no SDK and no client library.

The endpoint is read-only, open (no API key, no auth, no mutations), and introspectable. Everything below is the real behavior of this server, verifiable against a running instance.

Start it

From the repo root:

npm run graphql

That runs node graphql-server/index.mjs. On boot it reads every page under docs/ and prints where to reach it:

Keen Docs GraphQL API — 49 docs loaded
GraphiQL: http://localhost:4000/graphql
Endpoint: POST http://localhost:4000/graphql
  • GraphiQL (the in-browser explorer) is served at http://localhost:4000/graphql. Open it in a browser to browse the schema and run queries interactively. GET /graphql opens GraphiQL directly — the Yoga landing page is disabled.
  • The endpoint is the same URL via POST with Content-Type: application/json.
  • Port defaults to 4000 and is overridable with the PORT environment variable:
PORT=8080 npm run graphql

There is no build step and no database. The server walks the docs/ tree once at startup, parses frontmatter and headings, and serves the result from memory.

Where the code lives

Three small ES modules under graphql-server/:

FileResponsibility
loader.mjsWalks docs/, parses each .md/.mdx page (frontmatter, headings, body, word count) into immutable Doc records.
schema.mjsDefines the GraphQL typeDefs and resolvers over those records.
index.mjsBoots the GraphQL Yoga HTTP server, wires in graphql-armor, and listens on $PORT.

The schema

The documentation is the data: every page is a Doc.

type Doc {
id: ID!
path: String!
slug: String!
title: String!
description: String
section: String!
category: String
headings: [Heading!]!
body: String!
wordCount: Int!
}

type Heading {
depth: Int!
text: String!
anchor: String!
}

type Section {
name: String!
docCount: Int!
docs: [Doc!]!
}

type ApiMeta {
name: String!
description: String!
docCount: Int!
sections: [String!]!
endpoint: String!
}

type Query {
docs(section: String, search: String, limit: Int = 100): [Doc!]!
doc(id: ID, slug: String): Doc
sections: [Section!]!
search(query: String!, limit: Int = 20): [Doc!]!
meta: ApiMeta!
}

There is no Mutation type — this endpoint is read-only by design.

Try a query

meta is the cheapest way to confirm the server is up and see what it holds:

curl -sS http://localhost:4000/graphql -H 'content-type: application/json' \
--data '{"query":"{ meta { name docCount sections endpoint } }"}'
{
"data": {
"meta": {
"name": "Keen Docs GraphQL API",
"docCount": 49,
"sections": ["chat", "graphql", "keen-extension", "onboarding", "organization", "root", "tooling"],
"endpoint": "/graphql"
}
}
}

Full-text search returns the pages whose title, description, or body match — ask only for the fields you want back:

curl -sS http://localhost:4000/graphql -H 'content-type: application/json' \
--data '{"query":"{ search(query: \"introspection\", limit: 3) { id title slug } }"}'
{
"data": {
"search": [
{ "id": "graphql/agents", "title": "Drive the docs from an agent", "slug": "/docs/graphql/agents" },
{ "id": "graphql/errors-and-limits", "title": "Errors and limits", "slug": "/docs/graphql/errors-and-limits" },
{ "id": "graphql/index", "title": "GraphQL API", "slug": "/docs/graphql" }
]
}
}

Filter a section with docs, or pull a single page (with its heading outline) by id or slug:

query {
docs(section: "graphql") {
id
title
}
doc(id: "graphql/index") {
title
section
wordCount
headings {
depth
text
anchor
}
}
}

The graphql section currently holds 6 docs. And sections gives you the whole map at a glance:

curl -sS http://localhost:4000/graphql -H 'content-type: application/json' \
--data '{"query":"{ sections { name docCount } }"}'
{
"data": {
"sections": [
{ "name": "chat", "docCount": 7 },
{ "name": "graphql", "docCount": 6 },
{ "name": "keen-extension", "docCount": 15 },
{ "name": "onboarding", "docCount": 10 },
{ "name": "organization", "docCount": 9 },
{ "name": "root", "docCount": 1 },
{ "name": "tooling", "docCount": 1 }
]
}
}

Access and limits

This is a public, open, read-only API — there is no API key and no authentication of any kind. It only ever exposes already-published documentation, so it is safe to point an agent at.

  • Introspection is on and GraphiQL is on, so an agent or developer can fetch the full schema from the endpoint and write correct queries cold.

  • Bounded complexity. Every operation is capped by the graphql-armor EnvelopArmorPlugin. These are the exact limits configured in graphql-server/index.mjs:

    GuardLimit
    maxDepth10
    maxAliases20
    maxTokens1500
    costLimit (max cost)8000
    blockFieldSuggestionoff

A query that exceeds a limit is rejected before execution rather than partially run. For example, asking for an unknown field returns a validation error:

{
"errors": [
{
"message": "Cannot query field \"bogus\" on type \"ApiMeta\".",
"extensions": { "code": "GRAPHQL_VALIDATION_FAILED" }
}
]
}

Exceeding 20 aliases fails with Aliases limit of 20 exceeded, and request batching (a JSON array body) is rejected with Batching is not supported. (code: BAD_REQUEST). Field suggestions are deliberately left on (blockFieldSuggestion: false) — on a public docs API a "did you mean…?" hint is worth more than the minor information-hiding it would buy.

Deploying it

The Docusaurus site is staticnpm run build emits plain HTML/JS/CSS, and a static host cannot run resolvers. The GraphQL endpoint is a separate concern.

graphql-server/index.mjs is a standalone Node process (a graphql-yoga server on Node's built-in http). Deploy it as a small long-running service (a container or a process behind your reverse proxy) or wrap the Yoga handler in a serverless function — Yoga exports a standard request handler — and run it alongside the static docs site. Route /graphql to this service and serve the static build for everything else. Set PORT from the environment and you are done: there is no other configuration and no persistent state, since the doc records are rebuilt from the filesystem on each cold start.

Visualize the whole site

The server also serves a live connection graph of the entire docs site — every page as a node (colored by section), every cross-link as an edge — at:

http://localhost:4000/graph

It is a single static page (graphql-server/graph.html) that runs one graph query against the endpoint and renders it with vis-network. Open it after npm run graphql to see the documentation as a graph.