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 /graphqlopens GraphiQL directly — the Yoga landing page is disabled. - The endpoint is the same URL via
POSTwithContent-Type: application/json. - Port defaults to
4000and is overridable with thePORTenvironment 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/:
| File | Responsibility |
|---|---|
loader.mjs | Walks docs/, parses each .md/.mdx page (frontmatter, headings, body, word count) into immutable Doc records. |
schema.mjs | Defines the GraphQL typeDefs and resolvers over those records. |
index.mjs | Boots 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 ingraphql-server/index.mjs:Guard Limit 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 static — npm 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.