Knowledge API Service
1. Status
Section titled “1. Status”This decision is Accepted. services/knowledge-api is implemented, depends only on builders/core, and runs as a second process alongside the existing static site, reachable exclusively over Fly’s private 6PN network.
2. Context
Section titled “2. Context”The “Expose Ocean-Atlas Knowledge APIs” epic requires programmatic access to
Ocean-Atlas knowledge for Ocean Studio, Ocean CLI, Ocean Assistant,
generators, and compiler diagnostics — consumers that should not need to
depend on the website. adr-atlas-builder-boundary already anticipated
this work and deliberately scoped “a runtime Atlas API” out of Core V1,
calling it a “future story.” adr-search-today-structured-retrieval-tomorrow
separately requires that whenever that future retrieval work happens, it
must consume Core’s normalized AtlasModel — never scrape rendered
dist/ HTML, never re-parse canonical Markdown directly.
Between those two ADRs and this one, rfc.versioned-atlas-model (Draft,
2026-08-14) proposed a third path: publish a versioned, static JSON
projection of AtlasModel at build time (/api/atlas-model/v1.json),
served by the existing static site with no new runtime component. That
RFC explicitly considered and rejected a runtime query API
(§7.1, “Build a runtime query API instead of a static artifact”),
reasoning that “a live API is a larger infrastructure commitment… than
is justified before any concrete consumer exists.” At the time it was
written, that was correct — no consumer had been named.
This epic now supplies exactly the concrete consumer set the RFC’s
rejection was conditioned on: Ocean Studio, Ocean CLI, Ocean Assistant,
generators, and compiler diagnostics, each wanting live, queryable access
(including real server-side search with query parameters, not a
client-downloaded static blob) rather than a file they would each have to
fetch and index themselves. The RFC’s static-artifact proposal remains a
reasonable design for a different problem — offline or build-time
consumers with no need for live queries — but it does not serve this
epic’s actual consumers, and continuing to leave it as an open,
undecided Draft while implementing a different approach in parallel would
leave the repository’s own decision record silently contradicting itself.
This ADR resolves that directly: it supersedes rfc.versioned-atlas-model
rather than leaving it unaddressed.
3. Decision
Section titled “3. Decision”services/knowledge-api is a new runtime HTTP service that depends only
on builders/core’s buildAtlasModel — never canonical Markdown
directly, never generated presentation output (dist/, .generated/).
This extends adr.atlas-builder-boundary’s one-directional dependency
rule to a second, runtime downstream consumer: the build-time presentation
adapter (builders/astro-starlight) and this runtime service are siblings
downstream of Core, and neither depends on the other.
- Snapshot-per-process.
buildAtlasModel({ repositoryRoot })runs once at process startup; there is no live filesystem watching in production. A content change requires a redeploy — consistent with the deterministic, reproducible-build requirementadr.atlas-builder-boundary§17 already establishes, and withfly.toml’s existingstrategy = "immediate"redeploy model. - Versioned
/v1route prefix. Breaking changes require a new/v2prefix; additive changes (new fields, new routes) stay withinv1. Every response echoesAtlasModel.schemaVersionandapiVersion. - Network-private by construction, not by application-level
authentication. The service is reachable only via Fly’s private 6PN
network, at
http://ocean-atlas.internal:8080/v1— the same convention already used by other Ocean services (ocean-audit.internal:8080/v1,ocean-bundle.internal:8080/v1). It is deliberately never declared as a public[http_service]/[[services]]block infly.toml; that omission, not an authentication check, is what keeps it off the public internet. There is no application-level authentication for v1 — every route isGET-only and read-only, and the trust boundary is “any Machine on the Ocean Fly organization’s private network,” matching how sibling Ocean services already operate. - Deployment topology. Same Docker image and Fly Machine as the
public website. The node process binds
0.0.0.0:8080(6PN traffic arrives at the Machine’s private address, not loopback), but that port is simply never declared public — nginx and its public port-80 site are otherwise untouched. - No HTTP framework. A ~15-route,
GET-only surface is served by a small hand-rollednode:httprouter, matchingbuilders/core’s zero-runtime-dependency precedent.
4. Alternatives Considered
Section titled “4. Alternatives Considered”4.1 A static AtlasModel projection (rfc.versioned-atlas-model)
Section titled “4.1 A static AtlasModel projection (rfc.versioned-atlas-model)”This is the alternative this ADR formally supersedes. A build-time static
JSON artifact remains a smaller, purely additive change with no new
runtime process, and is worth reconsidering later for consumers that
genuinely want an offline or build-time snapshot rather than a live query
interface. It does not, however, serve this epic’s named consumers, who
need live search and filtering rather than a downloadable blob they would
each have to index themselves. Superseded here, not rejected outright —
the RFC’s own document is kept as historical proposal context per
decisions.rfcs.index’s lifecycle rules, marked Superseded with a
superseded-by relationship to this ADR.
4.2 A separate Fly app
Section titled “4.2 A separate Fly app”Rejected. ocean-atlas.internal already gives the right service identity
under the existing ocean-<name>.internal convention; a second app would
double the deployment surface for no isolation benefit at this scale.
4.3 Public exposure via an nginx reverse proxy on the same public port
Section titled “4.3 Public exposure via an nginx reverse proxy on the same public port”Considered and rejected after explicit direction: the API must not be
internet-reachable, only reachable from other Ocean services over Fly’s
private network. An earlier version of this design proxied /api/*
through nginx on the public site’s port; that would have made the API
publicly fetchable by anyone who could reach the website, which is
exactly what this decision exists to avoid.
4.4 Astro SSR or Astro API routes
Section titled “4.4 Astro SSR or Astro API routes”Rejected. Entangles a presentation framework with non-web consumers (the
Ocean CLI, compiler diagnostics) that have no reason to depend on it, and
breaks the fully-static dist/ output model adr.atlas-builder-boundary
establishes for the website.
4.5 builders/astro-starlight consuming the new API at build time
Section titled “4.5 builders/astro-starlight consuming the new API at build time”Rejected for now, despite this epic’s own note that “the website should
consume these APIs rather than directly accessing the underlying
knowledge source where practical.” Making the static build depend on a
running HTTP service would make it network-dependent and
non-deterministic, contradicting adr.atlas-builder-boundary §17’s
requirement that deleting generated content and dist/ and rebuilding
must reliably recreate the site. It would also be architecturally odd now
that the API is private-network-only: a public static build cannot depend
on a private-network-only service anyway. Recorded here as a known,
deliberately unresolved tension rather than silently promised or silently
implemented.
4.6 A full external API gateway or auth provider
Section titled “4.6 A full external API gateway or auth provider”Rejected. No such gateway exists yet for Ocean-Atlas to sit behind; the Fly private-network boundary is the gateway substitute for v1.
4.7 Live filesystem watching in production
Section titled “4.7 Live filesystem watching in production”Rejected. Contradicts the static-build determinism this repository already relies on, and turns a bad edit into a runtime crash instead of a controlled build failure.
4.8 A heavier HTTP framework (Express, Fastify, etc.)
Section titled “4.8 A heavier HTTP framework (Express, Fastify, etc.)”Rejected. The route count and complexity do not justify a new dependency on what is Ocean-Atlas’s first public-facing runtime process; a small hand-rolled router is easier to audit.
4.9 Semantic or embedding search now
Section titled “4.9 Semantic or embedding search now”Rejected. atlas.knowledge-architecture §20 already treats semantic
search as a distinct future layer on top of keyword search, not a v1
requirement; no proven need exists yet.
5. Consequences
Section titled “5. Consequences”Benefits
Section titled “Benefits”- Ocean Studio, Ocean CLI, Ocean Assistant, generators, and compiler diagnostics gain live, queryable access to Ocean-Atlas knowledge without depending on the website or re-implementing Markdown parsing.
- The one-directional dependency rule from
adr.atlas-builder-boundarynow has a second proof point: a runtime consumer can sit downstream of Core exactly like a build-time adapter does, without either depending on the other. - No new public attack surface: the API is unreachable from the public internet by construction (absence of a public Fly service declaration), not by a security control that could be misconfigured.
- No new Fly app or infrastructure spend — the same Machine and Docker image serve both the public site and the private API.
- The repository’s decision record stays internally consistent: the contradiction between this ADR and the Draft RFC is resolved explicitly rather than left standing.
Trade-offs
Section titled “Trade-offs”- This is Ocean-Atlas’s first long-running (non-static-build) process. If it crashes, nginx keeps serving the public site fine, but the API becomes unreachable until the container restarts — there is no supervisor restarting just the node process (see Implementation Implications).
- Any Machine on the Ocean Fly organization’s private network can call the API unauthenticated. This mirrors how sibling Ocean services already operate, but is a real trust boundary, not a hardened one; if it ever proves insufficient, a future story would need to add a shared-secret or token check.
builders/astro-starlightstill callsbuildAtlasModeldirectly at build time rather than through this API (see 4.5), so this epic’s “website should consume these APIs where practical” note is only partially honored — deliberately, and recorded as such rather than silently dropped.- Superseding
rfc.versioned-atlas-modelhere means its static-artifact proposal is not being built now. If a future consumer genuinely needs an offline/build-time snapshot rather than a live API, that proposal’s design remains a reasonable starting point, not wasted work.
Follow-up Consequences
Section titled “Follow-up Consequences”decisions/rfcs/rfc-versioned-atlas-model.md’sStatusis updated toSupersededwith asuperseded-by: adr.knowledge-api-servicerelationship, anddecisions/rfcs/index.md’s table is updated to match, perdecisions.rfcs.index’s own stated lifecycle (Draft → Review → Accepted / Rejected / Withdrawn → Superseded).
6. Implementation Implications
Section titled “6. Implementation Implications”services/knowledge-apimay depend only on@ocean-atlas/builder-core; a future change that reads canonical Markdown directly, or readsbuilders/astro-starlight’sdist//.generated/output as this service’s data source, does not comply with this decision.docker/nginx.confandfly.toml’s public[http_service]block must remain untouched by this service — no/api/or/v1/proxy location, no second public service declaration. Adding either would silently reintroduce public exposure and must be treated as a regression against this ADR, not a routine change.- The node process must bind
0.0.0.0(required for Fly 6PN reachability) but must never be declared in a public Fly service definition. - A route introduced in
/v1must not change or remove an existing response field without a version bump to/v2. - Unlike the static site, this service calls
buildAtlasModel()at process startup, so the runtime image must include the canonical Markdown source itself (and theocean-examplessubmodule), not just the generated site.Dockerfile’s runtime stage copies these explicitly, mirroringCANONICAL_DIRECTORIESinbuilders/core/src/discover.ts; a future new canonical top-level directory needs a matching line there, or the API will silently omit its content rather than fail the build.
7. Validation and Compliance Rules
Section titled “7. Validation and Compliance Rules”- A future change that makes
services/knowledge-apireachable from the public internet — directly, or by proxying it through nginx’s public port — does not comply with this decision unless a new ADR explicitly revisits the private-network boundary. - A future change that has
services/knowledge-apiread canonical Markdown or generated presentation output instead ofbuilders/core’sAtlasModeldoes not comply with this decision. rfc.versioned-atlas-modelmust not be silently rewritten to describe this implementation; it remains historical proposal context, markedSuperseded.
8. Related Knowledge
Section titled “8. Related Knowledge”adr.atlas-builder-boundary— the Core/adapter one-directional dependency rule this decision extends to a second, runtime consumer, and the ADR that originally deferred “a runtime Atlas API” as a future story this ADR now delivers.adr.search-today-structured-retrieval-tomorrow— requires future retrieval to consume Core’s structured model rather than scrape rendered output; this service is the first concrete implementation of that constraint.adr.stable-knowledge-identity— the stable IDs this service’s by-ID lookup, relationship resolution, and search results depend on.rfc.versioned-atlas-model— the static-artifact proposal this decision supersedes, kept as historical proposal context.atlas.knowledge-architecture— §20–21 describe the retrieval and AI/RAG readiness goals this service is a concrete step toward.architecture.knowledge-api— the reference document describing this service’s route table, envelope, and network stance.
These semantic relationships are declared in the document metadata.