Skip to main content

One core, three adapters

All the logic lives in src/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 like color.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.