One core, three adapters
All the logic lives insrc/lib/core/. The REST API, the MCP server and the
CLI are thin adapters over it, and the web app is a client of the REST API
like any other. If the app needs something the API can’t do, the API isn’t
finished.
The API
/api/v1 is versioned and public. Every request carries an API key
(Authorization: Bearer ab_...) or a session from the app. A key works in one
workspace with one scope, and scopes are a ladder:
The spec at
/api/v1/openapi.json is generated from the same Zod schemas the
handlers validate with, so it can’t drift from the code. It is the
API reference here too.
Bytes
Uploads go straight from the client to object storage through a presigned PUT; the server never proxies them. The server then hashes what arrived (SHA-256) and files it under that hash, so identical bytes are stored once. Delivery is a URL:/a/{id} is the original, /a/{id}/w_800,f_webp a
rendition, made on first request and cached in storage for 30 days. Only an
approved, unexpired asset is public: an archived, expired or deleted one
answers 410 Gone, so embeds stop on time.
The canon
Brand rules are structured records, not a PDF: a dotted key likecolor.primary, a typed value, a sentence on how to use it, and the assets it
points at. A rule can have a variant per context (dark-background,
instagram-story). The guidelines page is drawn from these records, and
agents read the same records over MCP.
Agents
/api/v1/mcp is an MCP server over Streamable HTTP. Chat apps and coding
agents connect with the URL alone: OAuth sends the person to a consent screen,
and the agent gets a key bound to them, never able to do more than they can.
What an agent adds lands proposed, for a person to approve.