ADR-0003: A versioned public REST API

Edit this page

Status: Proposed Date: 2026-08-14

Context

Loomscope already exposes REST endpoints under /api/v1/, but they exist to serve daemons and the application's own UI. There is no supported way for an operator to ask "which hosts appeared this week" from a script, feed inventory into a CMDB, or drive Loomscope from a scheduled job.

Three consumers are already visible:

  • Operators wanting to script routine questions, or export inventory into a system of record.
  • Automation platforms — n8n, Zapier, plain cron — that need a stable HTTP surface.
  • Loomscope's own future clients, specifically the CLI (ADR-0004) and the MCP server (ADR-0005). Both are API consumers. Building either before the contract is stable would bake an unstable interface into a shipped tool.

Two assets already exist and make this cheaper than it looks. packages/contracts holds Zod schemas that generate an OpenAPI 3.1 document, and Better-Auth provides an API-key plugin. What is missing is not plumbing but a commitment: an interface other people may depend on.

Decision

Promote a defined subset of /api/v1/ to a supported public API, with the compatibility guarantees that phrase implies.

Scope. Read access to the inventory — hosts, services, ports, networks, sites, certificates, vulnerabilities, snapshots and topologies — plus the write operations an operator genuinely automates: registering a network, triggering a scan, acknowledging a vulnerability, taking a snapshot. Everything else stays internal and is documented as such.

Authentication uses API keys issued per organisation, with the lsk_ prefix reserved for user keys (lsd_ already denotes daemon keys). Keys are hashed at rest, carry explicit scopes, and are CSRF-exempt because they are not cookies. A key never grants more than the membership that created it.

Versioning. /api/v1/ is frozen for additive change only: new endpoints and new optional response fields are allowed; removing a field, changing a type or tightening validation is not. Breaking changes go to /api/v2/, and v1 then gets a deprecation window with a Sunset header.

The contract stays generated. Zod remains the source of truth. The OpenAPI document is published at /api/openapi.json and served as browsable documentation. Any endpoint absent from that document is not public.

Errors are typed and stable — a machine-readable code, a human message and a requestId, using RFC 9457 problem details.

Everything is org-scoped and paginated. Cursor pagination, a default page size, and an enforced maximum. Row-level security applies to API keys exactly as it does to sessions.

Alternatives considered

GraphQL. Rejected. It suits clients that need to shape arbitrary queries, which is not the observed need — the demand is for a few well-known questions answered reliably. It would also require a second schema pipeline alongside the Zod-to-OpenAPI one, and gives an unbounded query surface on a database holding an entire estate's inventory.

Expose tRPC. Rejected. tRPC is excellent for a TypeScript client talking to a TypeScript server and poor as a public contract: it is not language-neutral, has no standard documentation format, and would tie the public interface to an internal implementation detail. (The tRPC surface in the codebase is presently an 8-line stub in any case.)

Publish the existing internal endpoints as they are. Rejected. They were shaped for the UI's needs, return whatever the page happened to require, and have no pagination contract. Publishing them would freeze accidents into a compatibility promise.

Do nothing; users have SQL. Rejected. It works — PostgreSQL is right there — but it couples every integration to the physical schema, which migrations then cannot change. An API exists precisely so the schema can.

Consequences

Accepted costs.

  • Compatibility becomes a constraint. Once v1 is public, refactoring is no longer free, and some future schema change will need a translation layer.
  • Rate limiting, key rotation and scope management become real features with real UI.
  • The public surface is a security surface. It needs its own tests, and the scoping has to be right.

Gains.

  • The CLI and MCP server become thin clients over a stable contract instead of three parallel implementations.
  • Integration with a CMDB or a ticketing system stops requiring database access.
  • The OpenAPI document is already generated, so interactive documentation is nearly free.

Sequencing. This lands before ADR-0004 and ADR-0005. Both depend on it.