# Architecture (/docs/architecture) The [Overview](/docs) makes the case for why this is worth building on. This page is how it actually works: one application, two modes, three surfaces, and a pipeline that moves data from Figma into everything downstream. ## What Handoff 2 is [#what-handoff-2-is] Handoff is **one Next.js application, `handoff-app`**. There is no separate "server" package; the same codebase runs a developer's local preview, pushes to a deployed registry, and serves that registry back out over UI, REST, and MCP. It depends on **`handoff-core`** for the token engine: DTCG parsing, alias resolution, and the multi-axis (brand × scheme) token model that underpins the [token pipeline](/docs/workspace/token-pipeline). ## The two run modes [#the-two-run-modes] Every `handoff-app` deployment is one of exactly two things: * **Workspace**: a client project repo (e.g. `/handoff/`), run locally via the `handoff-app` CLI. It reads and writes the filesystem: component sources, `handoff.config.js`, and a `design-system/` token pipeline. A workspace **never deploys itself**. It *pushes* to a registry over HTTP (`handoff-app push:all`). * **Registry**: a clean, standalone `handoff-app` deployment (typically on Vercel) with **no client-specific data at deploy time**. It stores everything a workspace pushes to it in **Postgres**, and serves it back out to stakeholders and API/MCP consumers. ### The `DATABASE_URL` switch [#the-database_url-switch] Both modes are the *same code path*, branching on one thing: whether a database connection string is configured at startup. ``` getDataProvider() → DATABASE_URL set → DynamicDataProvider (registry: reads/writes Postgres) → DATABASE_URL absent → StaticDataProvider (workspace dev: reads the local filesystem) ``` All page components, REST routes, and MCP tools read through `getDataProvider()`, never directly from the filesystem or the database, so the same rendering and business logic works identically in both modes. See [Workspace → Local preview](/docs/workspace/local-preview) for what this looks like day to day, and [Registry → Model](/docs/registry/model) for the full `DataProvider` writeup. MCP and the reference API are **unauthenticated in workspace mode** (unless `HANDOFF_SYNC_SECRET` is set) and **require a bearer token in registry mode**. See [MCP → Authentication](/docs/mcp/auth). ## The three-surfaces principle [#the-three-surfaces-principle] Every canonical data type in Handoff, tokens, components, icons, pages, design artifacts, and so on, is meant to be reachable through three surfaces: 1. **UI**: a foundation or component page a human can browse. 2. **REST**: a documented [OpenAPI](/docs/api/openapi-spec) route a script or another service can call. 3. **MCP**: an agent-callable tool exposing the same data, shaped for a model's context window. When a new data type is added to Handoff, all three surfaces are expected to grow together. Each one reads from the same `getDataProvider()` call, not a separate copy of the data, which is what keeps them from drifting apart. See [MCP](/docs/mcp) for the agent-facing surface, and [Concepts](/docs/concepts) for a per-entity breakdown of all three surfaces side by side. ## The canonical data model, at a glance [#the-canonical-data-model-at-a-glance] | Entity | What it is | Deep dive | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- | | **Tokens** | DTCG-format design tokens (color, typography, spacing, radius, grid, …), primitive + semantic tiers, multi-axis (brand/scheme) | [Concepts → Tokens](/docs/concepts/tokens) | | **Components** | Structured metadata + raw source (code/html/sass/css) + rendered build output | [Concepts → Components](/docs/concepts/components) | | **Patterns** | Saved compositions of component blocks (playground pages) | [Concepts → Patterns](/docs/concepts/patterns) | | **Pages / docs** | Markdown documentation pages (distinct from patterns) | [Concepts → Pages / docs](/docs/concepts/pages-docs) | | **Design artifacts** | Uploaded/generated design images with an AI-derived component spec | [Concepts → Design artifacts](/docs/concepts/design-artifacts) | | **Assets** | Icons, logos, and other brand assets (catalog + search) | [Concepts → Assets](/docs/concepts/assets) | | **Changelog** | Unified change history across all of the above | [Concepts → Changelog / audit](/docs/concepts/changelog-audit) | This table is the shape, not the full reference. [Concepts](/docs/concepts) covers what each entity actually looks like in Postgres and which UI/REST/MCP routes touch it. ## The workspace → registry pipeline [#the-workspace--registry-pipeline] Putting the two modes and three surfaces together, here's the full path a piece of data takes from Figma to an agent's context window: Concretely: `handoff-app fetch` pulls raw tokens out of Figma into the workspace filesystem. `npm run tokens:build` runs those tokens through `handoff-core`'s DTCG pipeline into compiled CSS/SCSS/Tailwind/DTCG output. `handoff-app push:all` then sends everything, tokens, components, pages, icons, logos, config, up to the registry over `POST /api/registry/*` and `POST /api/sync/upload`, where it lands in Postgres. From there, every read, a foundation page rendering, an OpenAPI route responding, an MCP tool answering an agent, goes through the same `getDataProvider()` call described above. Nothing downstream of the registry ever touches the workspace filesystem again. See [Workspace → Directory conventions](/docs/workspace/directory-conventions) for what lives on the left side of that diagram, and [API → Registry cluster](/docs/api/registry-cluster) / [API → Sync cluster](/docs/api/sync-cluster) for the push endpoints in the middle. ## Where to go next [#where-to-go-next] * **[Workspace](/docs/workspace)**: installing the CLI, `handoff.config.*`, directory conventions, the token pipeline, local preview. * **[Registry](/docs/registry)**: deploying a registry, first-admin setup, env vars, the stakeholder-facing UI. * **[API](/docs/api)**: the REST surface: OpenAPI spec, auth & scopes, and each route cluster. * **[MCP](/docs/mcp)**: the Model Context Protocol server: tool catalog, auth, onboarding. * **[Concepts](/docs/concepts)**: one page per canonical data type, showing its UI + REST + MCP surface together. # Overview (/docs) Handoff is design system documentation and automation, built to be read by more than just developers. A UI for humans to browse, an MCP server for AI agents to query, and a REST API for scripts and other services all read from the same underlying data. None of it is hand-written prose that quietly falls out of date the first time a component changes. You'd reach for it because most design system docs are written once and trusted forever, right up until the components change and nobody updates the page. Handoff inverts that: your tokens, components, pages, and assets live in one place and get served out fresh everywhere they're needed, so a developer reading the UI, an agent calling MCP, and a script hitting the REST API are all looking at the same facts. At a high level, here's how: a **workspace** (your project's own repo) pulls tokens from Figma and pushes everything it has, tokens, components, docs, icons, into a **registry**, a Postgres-backed deployment of the same application. The registry is the hub. The UI, the API, and MCP all read from it. [Architecture](/docs/architecture) is the deep dive on how that actually works; this page is the argument for why it's worth doing. ## Documentation and automation, for humans and agents alike [#documentation-and-automation-for-humans-and-agents-alike] Handoff isn't a docs site with an API bolted on afterward. Every piece of your design system, tokens, components, patterns, pages, assets, is meant to be reachable three ways: a UI a person can click through, an MCP server an agent can call, and a REST API anything else can hit. Add a token or ship a new component once, and all three surfaces pick it up, because they read from the same data instead of three separately maintained descriptions of it. See [Architecture → The three-surfaces principle](/docs/architecture#the-three-surfaces-principle) for how that's enforced in the codebase, not just claimed in a diagram. ## A data hub you own, not a rented corner of someone else's SaaS [#a-data-hub-you-own-not-a-rented-corner-of-someone-elses-saas] Your design system's data, tokens, component metadata, documentation, brand assets, lives in your own registry, backed by Postgres you control. It isn't sitting inside a third-party product you're renting seats on. You can query it, export it, point another tool at it, or build something new on top of it. Nothing here is locked behind someone else's UI or someone else's pricing tier. This is also the shape the rest of Handoff assumes: several sources feeding in, one hub holding the data, and several things built on top of it. Figma is the only source wired up today, and the design workbench, agents (MCP), and the CSS/SCSS/Tailwind/DTCG transpilers are live. The dashed nodes (additional sources, npm packages, prototyping, CMS destinations) are the direction the hub model points, not shipped connectors yet. ## Built for more than the design engineering team [#built-for-more-than-the-design-engineering-team] A design system only earns its keep if people beyond the two or three engineers who built it can actually use it. Handoff is built so a PM can browse the UI without asking someone to walk them through it, a non-developer can prototype an idea against real components instead of a mockup, and anyone curious can poke at the actual tokens and patterns rather than a screenshot of them. The more people who can build on a design system, the more it's worth having one. ## The payoff: automation that gets smarter as your data does [#the-payoff-automation-that-gets-smarter-as-your-data-does] This is where the hub model actually pays off. An MCP tool is only as good as the data behind it, and a lot of design-system MCP servers are thin wrappers around a handful of hand-maintained facts. Handoff's tools read the same rich, structured, constantly-synced data the UI and API do: real component metadata, real token values, real usage docs, kept current because pushing an update is part of the normal workflow, not a separate documentation chore. Better data means an agent can do more with it: generate a component that actually matches your system's conventions, answer a token question with the real value instead of a guess, or pull in a stack guide before writing a line of code. The more automated and accurate the data going in, the more capable the agent coming out. See [MCP → Tool catalog](/docs/mcp/tool-catalog) for what that looks like today. ## Where to go next [#where-to-go-next] * **[Architecture](/docs/architecture)**: how it actually works, the two run modes, the `DATABASE_URL` switch, the three-surfaces principle, and the workspace → registry flow. * **[Workspace](/docs/workspace)**: installing the CLI, project conventions, the token pipeline. Start here if you work in a client project repo. * **[Registry](/docs/registry)**: deploying a registry, first-admin setup, env vars, the stakeholder-facing UI. * **[API](/docs/api)**: the REST surface: OpenAPI spec, auth & scopes, and each route cluster. * **[MCP](/docs/mcp)**: the Model Context Protocol server: tool catalog, auth, onboarding. * **[Concepts](/docs/concepts)**: one page per data type, showing its UI + REST + MCP surface together. # Auth & Scopes (/docs/api/auth-scopes) Three credential types cover the whole API, but they're checked by **two different code paths** depending on the cluster, worth understanding, because it explains why some routes only care about one scope (`sync:write`) while others enforce the full vocabulary. ## The three credential types [#the-three-credential-types] | Type | Header | Used by | | ------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Sync bearer** | `Authorization: Bearer ` | CLI `push`/`push:all`/`pull`; either the raw `HANDOFF_SYNC_SECRET` value or a CLI OAuth JWT. | | **Session cookie** | NextAuth session (`__Secure-next-auth.session-token`) | Browser UI — every `/api/handoff/*` CRUD/admin/AI route. | | **OAuth JWT** | `Authorization: Bearer ` | MCP clients and the reference-materials API; issued by either OAuth flow in [`/api/oauth`](/docs/api/oauth-cluster), audience `handoff-api` (or the legacy `handoff-cli-sync`). | The spec's security-scheme section documents a third header, `X-Handoff-Api-Key`, for MCP and machine-to-machine calls. The actual auth code (`verifyHandoffApiAuth()`) only ever parses an `Authorization: Bearer` header; there's no `X-Handoff-Api-Key` check anywhere in the codebase. Use a bearer token for MCP, not that header. ## Two auth code paths [#two-auth-code-paths] **`/api/registry/*` and `/api/sync/*`** use `verifySyncAuth()` / `verifySyncBearer()` (`src/app/lib/sync-auth.ts`): accept the legacy `HANDOFF_SYNC_SECRET` bearer, or any valid CLI JWT. Write endpoints (`push` upload, every `/api/registry/*` `POST`) additionally require the JWT's `scp` claim to include `sync:write`; that's the only scope this path checks. **`/api/mcp` and the reference-materials API** (`/api/handoff/reference-materials`, the Figma plugin's foundations routes) use `verifyHandoffApiAuth()` (`src/app/lib/mcp-auth.ts`), which supports per-route `requireScopes` checks against the full vocabulary below, and additionally branches on run mode: * **Workspace mode** (no `DATABASE_URL`), unauthenticated by default, granted a read-only `admin`-role context (`sync:read reference:read components:read design:read`). Set `HANDOFF_SYNC_SECRET` to require a bearer even here. * **Registry mode** (`DATABASE_URL` set), a bearer is required; an unauthenticated request gets `401` with an RFC 9728 `WWW-Authenticate` header pointing at protected-resource metadata, rather than a bare rejection. Session-cookie auth for the registry's own stakeholder/admin UI (everything else under `/api/handoff/*`) is separate from both of the above, see [Registry → Auth](/docs/registry/auth). ## Scope vocabulary [#scope-vocabulary] Shared by both OAuth flows and by `verifyHandoffApiAuth()`'s `requireScopes` checks: | Scope | Covers | | -------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `sync:read` | Read sync cursor/changes. | | `sync:write` | Push sync changes; the only scope `/api/registry/*` and `/api/sync/upload` check. | | `reference:read` | Design guidelines, brand voice (`/api/handoff/reference-materials`). | | `components:read` | Reserved — not yet checked by any route. | | `components:write` | Reserved — not yet checked by any route. | | `design:read` | Component reference images. | | `design:write` | Reserved — not yet checked, but granted to member-role tokens by default. | | `generate:component` | Reserved — not yet checked by any route. | | `figma:sync` | Used by the Figma plugin's own REST endpoints (`/api/figma-plugin/*`), not by `/api/mcp` or any cluster in this pillar. | Role → scope defaults (`scopesForRole()`): an **admin** login receives every scope above; a **member** receives `sync:read reference:read components:read design:read design:write`. See [MCP → Authentication](/docs/mcp/auth) for the full walk-through of both OAuth flows that issue these JWTs, and [`/api/oauth`](/docs/api/oauth-cluster) for the endpoints themselves. # /api/handoff (app + AI + admin) (/docs/api/handoff-cluster) Everything in this cluster requires a NextAuth session cookie unless noted; this is the surface the registry's own UI is built on, not something a workspace's CLI calls directly (that's [`/api/registry`](/docs/api/registry-cluster) and [`/api/sync`](/docs/api/sync-cluster)). ## Content CRUD [#content-crud] | Resource | Endpoints | Notes | | -------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Pages | `GET/POST/DELETE /api/handoff/pages` | `GET` with `?slug=` returns one full page; without it, all page summaries. `POST` upserts by slug and fires an async nav-sync. | | Patterns | `GET /api/handoff/patterns` (+ `/{id}`, `/{id}/clone`) | `q`/`group`/`source` query filters on list. | | Assets | `GET/POST /api/handoff/assets` (+ `/{id}`, `/{id}/raw`, `/presign`, `/confirm`, `/collections`, `/usages`) | `presign`/`confirm` are the two-step upload flow for new media. | | Components | `GET/PATCH /api/handoff/components`, `POST /api/handoff/components/build` | `PATCH` (admin only) updates metadata; `build` enqueues a preview build. | | Component validation | `GET /api/handoff/components/validation` | Latest validator snapshot for a component id. Returns an empty array when no database is configured (the route's residual `isPostgres()` check, SQLite itself was removed, see `docs/SQLITE-REMOVAL-ADR.md`). | | Component history | `GET /api/handoff/components/history` | Push/version history, `limit` capped at 200. | | Changelog | `GET /api/handoff/changelog` | Unified time-ordered feed across components/tokens/pages. `since` (ISO 8601) + `limit`. Postgres only. | ## AI [#ai] | Endpoint | Notes | | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST /api/handoff/ai/chat` | Streaming (SSE) chat with the design-system assistant; tool calls like `show_components`, `navigate_component`, `get_recent_changes`, `check_validation`. Accepts an optional `pageContext` to scope tools to a component/pattern. | | `POST/GET /api/handoff/ai/generate-component` | Enqueues (and polls) an AI component-generation job from a design artifact. | Several more `ai/*` routes exist in code beyond what's in `openapi.yaml` today (design-artifact extraction/CRUD, generate-design, generate-pattern, foundation-preview, layout-guide analysis): internal/evolving surfaces, not detailed here; the ones above are the spec'd, stable pair. ## Figma [#figma] | Endpoint | Notes | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST/GET /api/handoff/figma/fetch` | Enqueues a Figma fetch job (rate-limited: 3/min, max 2 concurrent); `GET ?jobId=` polls status, omit for connection info. The server-side counterpart to a workspace's own `handoff-app fetch`. | | `GET /api/handoff/figma/components` | Figma component audit results — the registry-side counterpart to `handoff-app audit:figma-components`. | ## Admin [#admin] | Endpoint | Notes | | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `GET /api/handoff/admin/users` | List users. | | `POST /api/handoff/admin/invite` | Invite a user (`admin`/`member` role). | | `POST /api/handoff/admin/role` | Update a user's role. | | `POST /api/handoff/admin/remove` | Remove a user. | | `GET /api/handoff/admin/build-tasks` | The merged component-build + design-asset-extraction job queue. | | `GET/POST /api/handoff/admin/reference-materials` | Read, or regenerate, AI reference materials (design guidelines / brand voice). Session + `admin` role only, no bearer path. | All admin routes are documented as internal/subject-to-change pre-2.0. See [Registry → Auth](/docs/registry/auth) for who holds the `admin` role. ## Reference materials (bearer, not session) [#reference-materials-bearer-not-session] `GET /api/handoff/reference-materials` is the bearer-authenticated read of the same data: `verifyHandoffApiAuth()` with the `reference:read` scope (or the legacy sync secret), for MCP clients and integrations rather than the admin UI. `?id=` returns one material's full content; without it, the id/size/generated-at summary list. Read-only, regeneration stays on the admin route above. See [Auth & scopes](/docs/api/auth-scopes). ## Account [#account] `PUT /api/account`, update the signed-in user's own profile (`name`, `image`). ## Public component routes [#public-component-routes] Two routes outside `/api/handoff/*` need no auth at all: * `GET /api/components`: a flat list of every component summary in the registry. * `GET /api/component/{path}`: serves a single built component artifact (HTML preview, CSS, JS, JSON metadata, screenshot) by its path within the component bundle. Resolves from the database in registry mode, from disk in workspace mode. # API (/docs/api) Every registry publishes a full **OpenAPI 3.1 spec** describing its REST surface, the [three-surfaces principle](/docs/architecture#the-three-surfaces-principle) applied to any script or service that isn't a browser or an MCP client. See [OpenAPI spec](/docs/api/openapi-spec) for where it lives and how to browse it. ## Route clusters [#route-clusters] | Cluster | Auth | Covers | | ----------------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------- | | [`/api/registry/*`](/docs/api/registry-cluster) | Sync bearer (write); public (read) | The singleton config/theme/tokens/icons/etc. a workspace pushes via `push:all`. | | [`/api/sync/*`](/docs/api/sync-cluster) | Sync bearer | The component/pattern/page push-pull pipeline (`push`/`pull`/`sync-status`). | | [`/api/handoff/*`](/docs/api/handoff-cluster) | Session cookie | The registry's own app surface: content CRUD, AI, Figma, admin, account. | | [`/api/oauth/*`](/docs/api/oauth-cluster) | — | Device-code and OAuth 2.1 connector flows that issue the bearer tokens everything else uses. | | [`/api/mcp`](/docs/api/mcp-cluster) | Sync bearer / OAuth JWT | The MCP server endpoint — full tool surface documented in the [MCP pillar](/docs/mcp). | A few smaller, public read-only routes sit outside these clusters, `GET /api/components` (flat component list) and `GET /api/component/{path}` (serves a single built artifact file), see [`/api/handoff`](/docs/api/handoff-cluster#public-component-routes). See [Auth & scopes](/docs/api/auth-scopes) for how each of the three credential types works across every cluster above. # /api/mcp (/docs/api/mcp-cluster) A single route, `/api/mcp`, serves the entire MCP server over a **stateless Streamable HTTP** transport, no SSE, no session affinity. `GET`, `POST`, and `DELETE` all funnel into the same handler; see [MCP → Route & transport](/docs/mcp/route-transport) for the full detail (the transport class, why it's stateless, and how project context gets hydrated per call rather than per connection). Auth is the same `verifyHandoffApiAuth()` path described in [Auth & scopes](/docs/api/auth-scopes) — a sync bearer or an OAuth JWT, with per-tool scope checks happening inside individual tool handlers rather than at the route level. See [MCP → Authentication](/docs/mcp/auth) for the two run-mode behaviors (open by default in workspace mode, required in registry mode) and both OAuth flows that can obtain a token. The full tool surface, every tool, grouped by category, with its purpose and required scope, is documented in the [MCP pillar](/docs/mcp), not duplicated here. # /api/oauth (device flow + OAuth 2.1) (/docs/api/oauth-cluster) Two distinct flows issue the bearer JWTs used everywhere else in this API, one for a human at a terminal, one for a client that drives a normal browser OAuth redirect. Both are documented in full, with sequence detail, on [MCP → Authentication](/docs/mcp/auth); this page is the endpoint reference. ## Device authorization (RFC 8628), spec'd in openapi.yaml [#device-authorization-rfc-8628-specd-in-openapiyaml] Used by `handoff-app login`, `mcp-init`, and `mcp-token`. | Endpoint | Purpose | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST /api/oauth/device` | Starts a device-authorization request. Returns `{ device_code, user_code, verification_uri, verification_uri_complete }`. | | `POST /api/oauth/token` | Polled with `grant_type=urn:ietf:params:oauth:grant-type:device_code` + `device_code` until the user approves in a browser. Returns `{ access_token, token_type, expires_in }` on success, or an `authorization_pending`/`expired_token` OAuth error while waiting. | The user approves the request at `/cli/device` in a browser; the token that comes back is what gets saved to `.handoff/cli-auth.json`, see [Workspace → `.handoff/` runtime state](/docs/workspace/runtime-state). ## OAuth 2.1 connector flow, not yet in openapi.yaml [#oauth-21-connector-flow-not-yet-in-openapiyaml] Used by clients that register themselves dynamically and drive a standard authorization-code + PKCE redirect, e.g. a **claude.ai Connector**. These endpoints exist in code (`src/app/app/api/oauth/*`) but aren't yet documented in `public/openapi.yaml`: | Endpoint | Purpose | | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `GET /.well-known/oauth-authorization-server` (`authorization-server-metadata`) | RFC 8414 metadata a client uses to discover the other endpoints below. | | `GET /.well-known/oauth-protected-resource` (`protected-resource-metadata`) | RFC 9728 metadata; the `WWW-Authenticate` header on a `401` from `/api/mcp` points here. | | `POST /api/oauth/register` | RFC 7591 Dynamic Client Registration — no manual client setup on Handoff's side. | | `GET/POST /api/oauth/authorize` | The authorization redirect (`response_type=code`, `code_challenge_method=S256`); user approves here. | | `POST /api/oauth/device/approve` | Server-side approval step backing the human-facing `/cli/device` page. | `POST /api/oauth/token` is shared by both flows, it also accepts `grant_type=authorization_code` (with `code_verifier`) and `grant_type=refresh_token` for this flow, alongside the device-code grant type above. Both flows issue the same kind of JWT (`scp` scope claim, audience `handoff-api` or the legacy `handoff-cli-sync`), see [Auth & scopes](/docs/api/auth-scopes) for the scope vocabulary shared across everything that accepts one. # OpenAPI Spec (/docs/api/openapi-spec) ## Where it lives [#where-it-lives] * **Static file**: `public/openapi.yaml`, served at `/openapi.yaml` on any deployed registry. * **API route**: `GET /api/openapi` serves the same YAML with CORS headers, so it's the one to point Swagger UI / Redoc / Postman at from a browser or another origin. The route reads and serves the file directly, no codegen step. * Every registry page links to `/developer` in its footer, which is where the interactive API explorer lives; the raw YAML isn't linked from the footer directly. The spec is OpenAPI **3.1**, versioned independently of the app itself (`info.version: 2.0.0-alpha` at the time of writing), it tracks the pre-1.0 release series and will start incrementing on breaking changes once the API stabilizes. ## What it documents, and what it doesn't yet [#what-it-documents-and-what-it-doesnt-yet] The spec is the primary route reference for this pillar and covers every cluster below: registry push/read, sync, the session-authenticated `/api/handoff/*` surface (content CRUD, changelog, AI, admin, Figma, account), OAuth device authorization, and `/api/mcp`. `openapi.yaml` documents **40 paths**; `handoff-app` ships **131** route handlers under `src/app/app/api/`, and the spec was last touched on **2026-06-19**. It's a useful reference for the surfaces it does cover, but it is not a complete or current inventory, read the route handlers when the answer matters. Named gaps this pillar calls out where it's material, rather than inventing schema for what isn't spec'd: the modern **OAuth 2.1 connector flow** (`/api/oauth/register`, `/authorize`, `/authorization-server-metadata`, `/protected-resource-metadata`) exists in code (see [`/api/oauth`](/docs/api/oauth-cluster)) but isn't in `openapi.yaml`; most of the `ai/*` and `admin/*` surface is undocumented; `POST /api/sync/upload`'s documented `207` response is never returned by the code; and the Figma plugin's own REST surface (`/api/figma-plugin/*`) is a separate contract not covered by this spec or this pillar at all. ## Keeping it current [#keeping-it-current] Per `AGENTS.md`'s checklist for adding a data type: every new/changed route updates `paths:` and any new `components/schemas:` in the same PR, and `info.version` tracks the release milestone. There's no codegen, the spec is hand-maintained alongside the route handlers it describes. # /api/registry (/docs/api/registry-cluster) One push endpoint per data type, each writing into its own singleton row (or, for `theme.css`, serving a compiled artifact). All `POST` endpoints require a sync bearer with `sync:write`, see [Auth & scopes](/docs/api/auth-scopes). Called in sequence by `handoff-app push:all`, see [Workspace → CLI reference](/docs/workspace/cli-reference). | Endpoint | Read | Payload | | ------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `POST /api/registry/config` | `GET /api/registry/config` (public) | The `app` block from `handoff.config.*` — title, client, breakpoints, sort orders, etc. | | `POST /api/registry/theme` | `GET /api/registry/theme.css` (public, compiled CSS) | Theme CSS content. | | `POST /api/registry/navigation` | Served via `getDataProvider()` | The page navigation tree, derived from `pages/`. | | `POST /api/registry/pages` | `GET /api/registry/pages` (public, returns every page as a summary list, no query params) | All markdown pages + frontmatter. | | `POST /api/registry/tokens` | `GET /api/registry/tokens` | The raw Figma token snapshot (`localStyles.{color,typography,effect}`). | | `POST /api/registry/dtcg` | `GET /api/registry/dtcg` (+ `GET /api/registry/dtcg/download`) | The compiled DTCG manifest + `css`/`scss`/`tailwind`/`dtcg` dist output, see [Token pipeline](/docs/workspace/token-pipeline). | | `POST /api/registry/icons` | `GET /api/registry/icons` (+ `download`) | The icon catalog (`IconCatalogEntry[]`). | | `POST /api/registry/logos` | `GET /api/registry/logos` (+ `GET /api/registry/logo.svg`) | The logo set (`LogoSet`). | | `POST /api/registry/fonts` | — | Font files served back at `/fonts/`; consumed by `theme.css` and foundation raster previews. | A few endpoints outside the `push:all` table above cover narrower, related data: `POST /api/registry/assets/ingest` (and `/ingest-figma-fills`) is the per-image push path used by the image-reference rewrite system during component push (see [`/api/sync`](/docs/api/sync-cluster)); `POST /api/registry/image-slots` pushes image slot sizing specs extracted from Figma; `GET`/`POST /api/registry/components/{id}/previews` manage individual component preview records directly. ### `GET /api/registry/config` is a public read [#get-apiregistryconfig-is-a-public-read] Unlike the write side, `GET /api/registry/config` needs no auth at all; it's called from the client bundle for runtime branding (title, theme, etc.), so it has to be reachable before any session exists. ### Example: pushing config [#example-pushing-config] ```bash title="curl" curl -X POST https://your-registry.example.com/api/registry/config \ -H "Authorization: Bearer $HANDOFF_SYNC_SECRET" \ -H "Content-Type: application/json" \ -d '{"data": {"title": "My Design System", "client": "Acme Co."}}' ``` The `{ "data": { ... } }` envelope is specific to `/config`. Each other endpoint takes its own top-level key: | Endpoint | Body | | ------------------------------- | --------------------------------------------------------------- | | `POST /api/registry/config` | `{ data }` | | `POST /api/registry/theme` | `{ css }` (a string) | | `POST /api/registry/navigation` | `{ tree }` (a `NavigationNode[]`) | | `POST /api/registry/pages` | `{ pages }` | | `POST /api/registry/tokens` | `{ payload }` (+ optional `message`) | | `POST /api/registry/dtcg` | `{ manifest, css, scss, tailwind, dtcg }` (+ optional `brands`) | | `POST /api/registry/icons` | `{ catalog }` (an array) | | `POST /api/registry/logos` | `{ logoSet }` | | `POST /api/registry/fonts` | `{ fonts }` (an array) | Whatever the key, every one of these replaces the singleton row wholesale (no partial-merge semantics), re-running `push:all` is always safe. # /api/sync (push / pull pipeline) (/docs/api/sync-cluster) The low-level protocol behind `handoff-app push`/`pull`/`sync-status`, distinct from [`/api/registry`](/docs/api/registry-cluster)'s one-endpoint-per-data-type model. All three routes require a sync bearer (`verifySyncAuth()`, see [Auth & scopes](/docs/api/auth-scopes)); `upload` additionally requires `sync:write`. | Endpoint | Purpose | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /api/sync/status` | Connection health + current registry statistics — what `sync-status` prints. | | `GET /api/sync/changes?since=` | Returns every sync event recorded after `since` (a sync event ID, not a timestamp). `since=0` (the default) returns full history. This is what `pull` diffs against `.handoff/sync-state.json` to decide what to write back into the workspace. | | `POST /api/sync/upload` | Applies a batch of create/update/delete sync events — components, patterns, pages, source files, build artifacts, screenshots — from a `push`. | ## Upload semantics [#upload-semantics] `POST /api/sync/upload` takes `{ changes: SyncChange[] }` and applies them in order. The batch is **fail-fast**, not partial: the first bad or failing entry ends the request, and entries after it are never attempted. * **`200`**: `{ ok: true, appliedCount, applied: [...] }`, everything applied. * **`400`**: a malformed entry (missing `entityType`/`entityId`/`action`). Entries before it in the array have already been written. * **`500`**: an entry threw while being applied, `{ error, applied: [...] }` lists what did make it through before the failure. `openapi.yaml` still documents a `207` for this route, but the handler never returns one; there is no partial-success status code anywhere in the app. Treat a non-`200` as "the batch stopped part-way" and re-push. A component/pattern data change triggers an async validation snapshot on the registry side. ## Images ride along with component payloads [#images-ride-along-with-component-payloads] Component artifacts pushed through `upload` may reference local images (quoted strings, CSS `url()`). The CLI resolves these to local files, assigns content-addressed IDs (`img_`), rewrites the references in the pushed artifact text to `/api/handoff/assets/{assetId}/raw`, and pushes the images themselves separately via the per-image ingest endpoint (`POST /api/registry/assets/ingest`), so the registry always serves images from its own asset store, regardless of where they lived in the workspace. ## Relationship to `push:all` [#relationship-to-pushall] `push:all` covers config/theme/navigation/tokens/dtcg/icons/logos/fonts (all through [`/api/registry/*`](/docs/api/registry-cluster)), components and pages go through this cluster instead, as one of `push:all`'s steps (or standalone via plain `push`). See [Workspace → CLI reference](/docs/workspace/cli-reference) for every flag on both commands. # Assets (Icons, Fonts, Logos) (/docs/concepts/assets) "Assets" covers four related but structurally distinct entities. They share a theme (visual resources a design system publishes) but don't share one table or one push path. ## Shape [#shape] | Entity | Table(s) | Push | Notes | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **General assets** | `handoff_asset` (+ `handoff_asset_blob` for DB-stored bytes when S3 isn't configured, `handoff_asset_usage` for component↔asset usage tracking) | `POST /api/handoff/assets` (`presign`/`confirm` two-step upload) | `assetType` is `logo`\|`icon`\|`image`\|`video`; grouped by `handoff_asset_collection` (a Figma section or a manual group). | | **Icon catalog** | `handoff_registry_icons` — singleton row, full `IconCatalogEntry[]` | `POST /api/registry/icons` (`push:all`) | A separate, purpose-built catalog for icon browsing/search — distinct from individual `handoff_asset` rows of `assetType: 'icon'`, which back the general asset library instead. `handoff_icon_set` groups icons into named sets (e.g. a Figma component set). | | **Logo set** | `handoff_registry_logos` — singleton row, the full `LogoSet` shape | `POST /api/registry/logos` (`push:all`) | Light/dark/color/mono variants, primary/alternate/wordmark/icon-only — read by `/foundations/logo`. | | **Fonts** | `handoff_registry_font` — one row per font file (keyed by filename), not a true singleton | `POST /api/registry/fonts` (`push:all` step) | Confirmed as its own dedicated table (not folded into general assets): stores base64-encoded bytes, `family`/`familyKey`, `weight`, `style`, `format`. Served at `GET /fonts/` so `theme.css` `@font-face` rules and the foundation rasterizer (satori) resolve them without workspace filesystem access. | ## Three surfaces [#three-surfaces] | Surface | Where | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **UI** | General assets: `/assets`, `/assets/[id]`; fonts and logos get dedicated sub-views at `/assets/fonts`, `/assets/logos`. Icons and logos also render on their own foundation pages: `/foundations/icons`, `/foundations/icons/[name]`, `/foundations/logo`. See [Registry → Stakeholder-facing UI routes](/docs/registry/stakeholder-ui#foundations). | | **REST** | General assets: `GET/POST /api/handoff/assets` (+ `/{id}`, `/{id}/raw`, `/presign`, `/confirm`, `/collections`, `/usages`), see [API → `/api/handoff`](/docs/api/handoff-cluster#content-crud). Icon catalog / logo set: `POST`/`GET /api/registry/icons` (+ `/download`), `POST`/`GET /api/registry/logos` (+ `GET /api/registry/logo.svg`), see [API → `/api/registry`](/docs/api/registry-cluster). Fonts: push-only (`POST /api/registry/fonts`); read back via `GET /fonts/`, not a `/api/registry/*` route. | | **MCP** | Icons & Logos: `handoff_get_icon_catalog` (bulk, filterable by category), `handoff_search_icons` (free-text), `handoff_get_logo_set`. Assets (registry mode only): `handoff_search_assets`, `handoff_get_asset` (with component usages + size info), `handoff_list_asset_collections`. See [MCP → Tool catalog § Icons & Logos / Assets](/docs/mcp/tool-catalog#icons--logos). **No MCP tool currently exposes fonts**: the tool catalog has no font-specific entry; an agent that needs font data has to go through the REST/UI surface above. | `handoff_image_slot` records sizing specs (aspect ratio, recommended dimensions, scale mode) for named image-fill placeholders extracted from Figma inside a *component*; it's about how a component expects an image to be supplied, not an asset entity itself. See [Components](/docs/concepts/components) and `handoff-app`'s `schema-pg.ts` for its full shape. # Changelog / Audit (/docs/concepts/changelog-audit) Handoff tracks change history per entity type, then unifies it into one time-ordered feed: "what changed," across tokens/components/pages/patterns together, plus, where recorded, **why** it changed. ## Shape [#shape] Each entity type that participates keeps its own append-only change table, all following the same rough pattern, a `pushedAt` timestamp, who pushed (`pushedByUserId`/`pushedByName`), a `trigger` (`push`/`mcp`/manual), and a **"why"**: an optional human-authored `message` plus a lazily-generated `aiSummary`: | Table | Tracks | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `handoff_token_change` | Per-push token diffs — added/removed/modified counts and keys, plus actual before/after values for changed keys (bounded to what changed). | | `handoff_component_version` | One row per component push — a full metadata snapshot, a change summary, source-file hashes, and artifact filenames present at that version. | | `handoff_page_change` | Per-push page create/update/delete — title and markdown-length before/after. | | `handoff_pattern_change` | Per-push pattern create/update/delete — title, block count. | | `edit_history` | A generic `(entityType, entityId, diff)` log for edits that don't have a dedicated table above. | | `handoff_event_log` | The broadest audit log — auth events, fetch/build pipeline runs, and AI usage/cost (provider, model, estimated tokens/cost) across the whole registry, not scoped to a single content entity. Backs `/account/ai-cost` and `/admin/ai-cost`. | | `handoff_validation_run` | A related but distinct concept: append-only **health** snapshots (score, grade, per-validator/per-component breakdown), one row per push that included validation results. Not a change log — a quality trend, surfaced at `/system/health`. See [Components](/docs/concepts/components#shape). | ## Three surfaces [#three-surfaces] | Surface | Where | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **UI** | `/system/changelog` — the unified changelog view; `/system/health` for the separate validation-trend view, see [Registry → Stakeholder-facing UI routes](/docs/registry/stakeholder-ui#system). | | **REST** | `GET /api/handoff/changelog` — a unified, time-ordered feed across components/tokens/pages/patterns, filterable by `since` (ISO 8601) and `limit`. Postgres only (no equivalent in workspace dev, since there's no push history to unify without a registry), see [API → `/api/handoff`](/docs/api/handoff-cluster#content-crud) and [Workspace → Local preview § What's push-dependent](/docs/workspace/local-preview#whats-local-only-vs-push-dependent). | | **MCP** | The **Change Inquiry** family, registry mode only: `handoff_recent_changes` (recent changes across components/tokens/pages/patterns, newest first, with the "why" when available), `handoff_component_history` (one component's version history — what, who, when, why), `handoff_change_why` (the reason behind one specific change — the human message, or a cached AI-generated summary of the diff). See [MCP → Tool catalog § Change Inquiry](/docs/mcp/tool-catalog#change-inquiry). | ## Replacing the legacy changelog pages [#replacing-the-legacy-changelog-pages] The legacy site's `changelog/` section (four hand-maintained pages: overview, changelog, releases, versioning/stability notes) is dropped in favor of this unified, database-backed model going forward. Framework-level release notes, as opposed to a given registry's own content changes above, live in `handoff-app`'s own `Changelog.md` at the repository root, maintained ([Keep a Changelog](https://keepachangelog.com/) format, semantic versioning) rather than hand-duplicated here. # Components (Structured + Raw + Rendered) (/docs/concepts/components) A component in Handoff is three related but distinct facets of the same entity, not one blob: 1. **Structured**: the declaration: title, description, group, `properties` (the typed prop contract), and `previews` (named value-sets). Read/written as JSON. 2. **Raw**: the actual Handoff-layer source files (`.handoff.ts` declaration, templates, styles, scripts) pushed from the workspace, stored for display and for `pull`. 3. **Rendered**: the built preview artifacts (HTML/CSS/JS/JSON) produced by the workspace's build step and pushed alongside the declaration. This split mirrors the same authoring-spec-vs-canonical-record distinction the token pipeline uses for DTCG: a component author writes a friendly `defineComponent({...})` module in the workspace (the authoring adapter); what gets pushed and stored is the validated, three-facet canonical record. ## Shape [#shape] | Table | Facet | Holds | | --------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `handoff_component` | Structured | `id`, `path`, `title`, `description`, `group`, `type`, `properties`, `previews`, and a full `data` blob for round-trip. `source` marks provenance: `disk` (workspace filesystem), `db` (created in-app), or `figma`. | | `handoff_component_source` | Raw | One row per `(componentId, filePath)` — the actual `.handoff.ts`/template/style/script file contents pushed from the workspace. External dependencies (e.g. a shared UI package in a monorepo) are **not** stored here; those come from git. | | `component_artifact` | Rendered | One row per `(componentId, filename)` — built HTML/CSS/JS/JSON, populated only by the push API. The registry never builds a component itself; all builds happen locally in the workspace (see [Workspace → CLI reference](/docs/workspace/cli-reference#building--previewing)). | | `handoff_component_version` | History | One row per push, capturing a full metadata snapshot, a change summary, source-file hashes, and the pushed-by/"why" (message + lazy AI summary), see [Changelog / audit](/docs/concepts/changelog-audit). | | `handoff_component_preview` | Registry-authored previews | Distinct from the code-authored `previews` inside `handoff_component.data` (which are replaced wholesale on every push): these are named value-sets authored by a PM/designer/LLM directly in the registry, version-anchored to a `component_version` so they stay valid even after the contract moves on. | Each push can carry per-component validation results (accessibility, schema, contrast, or custom validators, see `handoff-app`'s ADR-002). These aren't a separate row per component; they roll up into the append-only `handoff_validation_run` health-snapshot log surfaced at `/system/health`, see [Changelog / audit](/docs/concepts/changelog-audit) for how change history and validation history relate. ## Three surfaces [#three-surfaces] | Surface | Where | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **UI** | `/system/component` (catalog) and `/system/component/[component]` (per-component doc + rendered preview), see [Registry → Stakeholder-facing UI routes](/docs/registry/stakeholder-ui#system). | | **REST** | Public: `GET /api/components` (flat catalog list), `GET /api/component/{path}` (serves one built artifact file). Session-authenticated: `GET/PATCH /api/handoff/components` (`PATCH` is admin-only metadata edit), `POST /api/handoff/components/build` (enqueue a preview build), `GET /api/handoff/components/validation`, `GET /api/handoff/components/history`, see [API → `/api/handoff`](/docs/api/handoff-cluster#content-crud). Push/pull specifically go through `POST /api/sync/upload` / `GET /api/sync/changes`, see [API → `/api/sync`](/docs/api/sync-cluster). | | **MCP** | `handoff_search_components`, `handoff_get_component` (slimmed for context — excludes compiled `sharedStyles` CSS, validation results, and Figma sync metadata), `handoff_get_component_reference` (style reference image for a slot), `handoff_get_reference` (generated reference material by id). Registry-authored previews: `handoff_create_preview`, `handoff_update_preview`, and `handoff_preview_component` (an interactive [MCP App](/docs/mcp/mcp-apps), not plain JSON). History: `handoff_component_history`, `handoff_recent_changes`, `handoff_change_why`. See [MCP → Tool catalog § Components / Previews / Change Inquiry](/docs/mcp/tool-catalog#components). | ## What v1's template pages become in v2 [#what-v1s-template-pages-become-in-v2] The legacy site's `managing_handoff/{handlebars,react,csf}_templates` pages described a materialize-per-client templating model, see [Registry → Model](/docs/registry/model#what-this-replaced). v2 replaces the bulk of that with the build/preview pipeline above plus MCP [stack guides](/docs/mcp/stack-guides) (per-stack authoring conventions an agent reads before generating a component). One CLI command from the old model still ships: `make:template [state]` scaffolds a new template for an existing component, see [Workspace → CLI reference § Eject / make](/docs/workspace/cli-reference#eject--make-customize-the-defaults). # Design Artifacts (/docs/concepts/design-artifacts) A **design artifact** is a saved output of the registry's AI design workbench: an image (an AI-generated composition, or an uploaded reference), the context that produced it, and, once extracted, a structured **component spec** an engineer or agent can build a real component from. It's a design-side reference a component can be generated from or linked back to, not a component or pattern itself. The design workbench needs `HANDOFF_AI_API_KEY` (or a cloud AI proxy) configured, see [Registry → Environment variables](/docs/registry/env-vars). Without it, `/design` and its MCP tools are unavailable. ## Shape [#shape] `handoff_design_artifact` carries the full workbench state for one artifact: `status` (`draft`/`review`/`approved`), the final `imageUrl` plus `sourceImages` (uploaded references), `componentGuides` (selected existing components used as style reference), `foundationContext` (a snapshot of foundations sent to the AI), `conversationHistory` (iteration-by-iteration prompt/image pairs), extracted `assets` + `assetsStatus`, and `componentSpec` (structured JSON, the part that connects this entity to [Components](/docs/concepts/components)) and `componentSpecMd` (an editable markdown version, authoritative after a user edits it) with its own `specStatus`. A `publicAccess` flag opts an artifact into the public share page. Two job tables back the async parts of this pipeline: `handoff_design_generation_job` (image generation) and `component_generation_job` (turning a spec into a real, built component, with an iteration loop bounded by `maxIterations` and a `visualScore` from vision-based comparison). ## Three surfaces [#three-surfaces] | Surface | Where | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **UI** | `/design` (the workbench itself), `/design/library` + `/design/library/[id]` (saved artifacts, browse/detail), `/design/library/[id]/share` (public share page — only reachable when `publicAccess` is set), `/design/assets` (extracted asset review), `/design/settings`, see [Registry → Stakeholder-facing UI routes](/docs/registry/stakeholder-ui#assets-design-and-playground). | | **REST** | A family of `POST/GET /api/handoff/ai/*` routes covers design-artifact extraction/CRUD and generation (`generate-design`, `generate-pattern`, foundation-preview, layout-guide analysis) — called out in [API → `/api/handoff`](/docs/api/handoff-cluster#ai) as an internal/evolving surface not yet fully spec'd in `openapi.yaml`, so it isn't enumerated route-by-route here. | | **MCP** | `handoff_list_design_artifacts`, `handoff_get_design_artifact`, `handoff_create_design_artifact` (requires a real user JWT — explicitly rejects the legacy sync secret, since it attributes authorship to a specific person), `handoff_get_component_spec` (the structured spec + editable markdown for a saved artifact), `handoff_generate_component_from_design` (fetch, or queue generation of, a spec + assets for local component generation). Registry mode only for most of these. See [MCP → Tool catalog § Design Artifacts](/docs/mcp/tool-catalog#design-artifacts). | ## Relationship to Figma MCP [#relationship-to-figma-mcp] `handoff-app`'s MCP RFC describes an illustrative "Figma → Handoff component" flow where a design agent combines Handoff's own context tools with a **separate Figma MCP server** (`get_design_context`, `get_screenshot`). Handoff deliberately doesn't embed a Figma client inside its own MCP server; Figma access is a different MCP server's job, composed by the agent. Design artifacts are Handoff's own path to the same destination (a built component from a design reference) that doesn't depend on a second MCP server being connected. # Concepts (/docs/concepts) Handoff's canonical data model is small: a handful of entity types, each stored in Postgres once a workspace pushes it, and each meant to be reachable through the same three channels, the [three-surfaces principle](/docs/architecture#the-three-surfaces-principle) introduced in [Architecture](/docs/architecture). This pillar is one page per entity, each covering: * **What it is**: the concept in plain terms, and how it differs from its neighbors. * **Shape**: grounded in the actual Postgres tables (`schema-pg.ts`), not an idealized model. * **Three surfaces**: where it shows up in the registry's own UI, which REST routes read/write it, and which MCP tools an agent calls to reach it, each linking out to the fuller reference on its owning pillar ([API](/docs/api) or [MCP](/docs/mcp)) rather than repeating it here. Use this pillar when you already know *what* you're looking for (a component, a token, an asset) and want all three surfaces side by side. Use [API](/docs/api) or [MCP](/docs/mcp) directly when you're starting from a route or a tool name instead. ## The entities [#the-entities] | Entity | What it is | Page | | --------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------- | | **Tokens** | DTCG-format design tokens — primitive + semantic tiers, multi-axis (brand × scheme). | [Tokens](/docs/concepts/tokens) | | **Components** | A component's structured metadata, raw source, and rendered build output, together. | [Components](/docs/concepts/components) | | **Patterns** | Saved compositions of component blocks (playground pages). | [Patterns](/docs/concepts/patterns) | | **Pages / docs** | Markdown documentation pages, pushed or agent-authored. | [Pages / docs](/docs/concepts/pages-docs) | | **Design artifacts** | AI design-workbench outputs — generated images with a derived component spec. | [Design artifacts](/docs/concepts/design-artifacts) | | **Assets** | Icons, logos, fonts, and general uploaded media. | [Assets](/docs/concepts/assets) | | **Changelog / audit** | Unified, cross-entity change history with a "why," not just a "what." | [Changelog / audit](/docs/concepts/changelog-audit) | ## How an entity gets here [#how-an-entity-gets-here] Every entity above follows the same lifecycle: authored or extracted in a workspace (or, for a few types, created directly by a stakeholder or an agent in the registry) → validated → pushed via [the sync or registry push APIs](/docs/api) → stored in Postgres → served back out through [the stakeholder UI](/docs/registry/stakeholder-ui), [REST](/docs/api), and [MCP](/docs/mcp). See [Registry → Model](/docs/registry/model) for the `DataProvider` mechanism that makes the same rendering code work whether the data came from Postgres or, in workspace dev, straight off disk. # Pages / Docs (/docs/concepts/pages-docs) A **page** (`handoff_page`) is a freeform markdown document, foundation overrides, guidelines, custom write-ups, distinct from every other structured entity in this pillar. It's pushed wholesale from a workspace's `pages/**/*.md` and can also be created or edited directly by a stakeholder or an agent once the registry exists. MCP's tool catalog has a category literally named **Pages & Compositions** (`handoff_list_pages`, `handoff_get_page`, `handoff_create_page`, `handoff_update_page`), those tools operate on **patterns** (saved component compositions from the Playground), not this entity. The markdown pages on this page are reached through MCP's separate **Documentation** tool family instead (below). See [Concepts → Patterns](/docs/concepts/patterns#three-surfaces) for the other side of this naming collision. ## Shape [#shape] `handoff_page` is keyed by `slug` (the path under `pages/`, e.g. `getting-started/install`) and holds `type` (`markdown` today; `mdx`/`html`/`plugin` reserved for future page-rendering stages per ADR-001 §7, see [Registry → Model](/docs/registry/model)), `frontmatter` (jsonb), `markdown` (the body), and `assets` (jsonb, for any per-page asset bundle, unused while `type` stays `markdown`). `handoff_page_change` is the parallel append-only audit table: one row per create/update/delete push, recording the action, title before/after, markdown-length before/after, and the same human-message + lazy-AI-summary "why" pattern used elsewhere, see [Changelog / audit](/docs/concepts/changelog-audit). ## Three surfaces [#three-surfaces] | Surface | Where | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **UI** | Rendered under the registry's catch-all page routes (`/[...slug]`) and, for foundation-specific overrides, under `/foundations/[...slug]` — falls back to `handoff-app`'s own bundled default markdown when a workspace hasn't pushed an override. Managed (create/edit/organize) at `/admin/pages`, see [Registry → Stakeholder-facing UI routes](/docs/registry/stakeholder-ui#admin-admin-role-required). | | **REST** | Session-authenticated CRUD: `GET/POST/DELETE /api/handoff/pages` (`GET ?slug=` for one full page, otherwise all summaries; `POST` upserts by slug), see [API → `/api/handoff`](/docs/api/handoff-cluster#content-crud). Workspace push/read: `POST /api/registry/pages` (all markdown + frontmatter, from `push:all`), `GET /api/registry/pages` (public, every page as a summary list — it takes no query params), see [API → `/api/registry`](/docs/api/registry-cluster). | | **MCP** | The **Documentation** tool family: `handoff_list_doc_pages`, `handoff_get_doc_page`, `handoff_create_doc_page` (`sync:write`), `handoff_update_doc_page` (`sync:write`), `handoff_delete_doc_page` (`sync:write`), `handoff_move_doc_page` (`sync:write`, renames a slug while preserving content/history), see [MCP → Tool catalog § Documentation](/docs/mcp/tool-catalog#documentation). More agent-authoring-oriented than the REST CRUD above: these are the tools an agent uses to write documentation directly into a registry. | # Patterns (/docs/concepts/patterns) A pattern is a **saved composition of component blocks**: an ordered arrangement of existing components with their prop values, built and saved from the [Playground](/docs/registry/stakeholder-ui#assets-design-and-playground) rather than declared as its own component. Where a component is a single reusable unit, a pattern is "here's how several of them fit together", a layout, not a primitive. ## Shape [#shape] `handoff_pattern` holds: `id`, `path`, `title`, `description`, `group`, `tags`, a `components` list (which component blocks it references), and a `data` blob (the full composition). `source` records how it *got here*: `playground` (built interactively), `build`, `import`, or `ai`. An optional `userId` records the creator/owner for playground-saved patterns. A second set of columns carries the sharing/review lifecycle: | Column | Values | | | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | | `kind` | What the row **is**, as opposed to where it came from: `page`, `template`, or `brief` (`brief` is transitional). Split out of `source`, which was answering two questions at once. | | | `status` | `prototype` \| `draft` \| `review` \| `approved` \| `archived`. Drives the review queue and `handoff_review_page`. | | | `visibility` | `private` \| `shared` \| `team` \| `public`. Never changed by a review decision — promoting a page to a wider audience stays a separate, deliberate act. | | | `shareLinkToken` | The guest share link a page was created through. Scopes a guest to their own submission, since guest pages are owned by the link's creator. Deliberately outlives the link, so there's no FK. | | | `templateId` / `submittedByEmail` | For a built page: the brief it came from (drives the review diff) and the author's email for state-change notifications. | Changes are tracked | | separately in `handoff_pattern_change`, one append-only row per create/update/delete, each | | | | carrying a human message and a lazy AI summary, the same "why" model components and tokens use | | | | — see [Changelog / audit](/docs/concepts/changelog-audit). | | | ## Three surfaces [#three-surfaces] | Surface | Where | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **UI** | `/patterns` (browse/search) and `/playground` (the interactive builder) for general use; `/system/pattern` and `/system/pattern/[pattern]` for the developer-facing catalog/detail view, see [Registry → Stakeholder-facing UI routes](/docs/registry/stakeholder-ui#assets-design-and-playground). | | **REST** | `GET /api/handoff/patterns` (+ `/{id}`, `/{id}/clone`), filterable by `q`/`group`/`source` on list, see [API → `/api/handoff`](/docs/api/handoff-cluster#content-crud). Also travels through `POST /api/sync/upload` / `GET /api/sync/changes` as a `pattern`-type sync change when pushed/pulled from a workspace (`handoff-app push --patterns `), see [API → `/api/sync`](/docs/api/sync-cluster). | | **MCP** | Confusingly named in the tool catalog: patterns are exposed under the **Pages & Compositions** tool family, not a `pattern`-named one — `handoff_list_pages`, `handoff_get_page`, `handoff_create_page` (`sync:write`), `handoff_update_page` (`sync:write`). The tool catalog itself calls this out: "'Pages' here means playground pages (patterns) — saved component-block compositions, distinct from markdown doc pages" (see [Concepts → Pages / docs](/docs/concepts/pages-docs) for that other, unrelated "pages" concept). See [MCP → Tool catalog § Pages & Compositions](/docs/mcp/tool-catalog#pages--compositions). | MCP's "Pages & Compositions" tools operate on **patterns** (this page). MCP's separate "Documentation" tool family (`handoff_list_doc_pages`, etc.) operates on markdown **doc pages** , see [Pages / docs](/docs/concepts/pages-docs). The REST layer avoids this collision (patterns live under `/api/handoff/patterns`, doc pages under `/api/handoff/pages`); only the MCP naming overlaps. Worth keeping straight when reading the tool catalog. # Tokens (DTCG) (/docs/concepts/tokens) Design tokens are Handoff's foundation-layer data: colors, typography, effects, spacing, radius, and grid values, extracted from Figma and/or hand-authored, normalized into the **DTCG 2025.10** format, and compiled to whatever a consuming stack needs. See [Workspace → Token pipeline](/docs/workspace/token-pipeline) for the full Figma → DTCG → dist walkthrough this page only summarizes. ## Shape [#shape] Two tables carry token data, at two different points in the pipeline: * **`handoff_tokens_snapshot`**: the raw Figma export as fetched (`localStyles.{color, typography, effect}`), one JSON `payload` per push. This is the input the rest of the pipeline builds on, not what pages read from. * **`handoff_registry_dtcg`**: the *compiled* output a workspace's `tokens:build` produces, pushed as one singleton row: `manifest` (the DTCG manifest), `css`/`scss`/`tailwind` (the compiled dist text), `dtcg` (alias-resolved JSON), plus `brands`, axis-aware token trees keyed by brand name (and scheme, for multi-axis projects), see [Registry → Singleton config rows](/docs/registry/singleton-config). Primitive vs. semantic tiers, and multi-axis (brand × scheme) resolution, are pipeline concepts covered in depth at [Workspace → Token pipeline](/docs/workspace/token-pipeline), this page is about where the *result* surfaces, not how it's produced. Some categories, spacing is the standing example, are hand-authored rather than Figma-extracted. The build step discovers existing hand-authored files and merges them into the manifest without clobbering them on the next `fetch`. See [Token pipeline → Build](/docs/workspace/token-pipeline#build-tokensbuild). ## Three surfaces [#three-surfaces] | Surface | Where | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **UI** | Foundation pages — `/foundations/colors`, `/typography`, `/spacing`, `/border-radius`, `/effects`, `/elevation`, `/grid`, `/motion` — each rendering a visual preview plus `TokenOutputTabs` (copy/download CSS/SCSS/Tailwind/DTCG) and a `ProvenanceBadge` (sync source, last-synced time). See [Registry → Stakeholder-facing UI routes](/docs/registry/stakeholder-ui#foundations). | | **REST** | `POST`/`GET /api/registry/tokens` (raw Figma snapshot) and `POST`/`GET /api/registry/dtcg` (+ `/download`) for the compiled manifest/dist, see [API → `/api/registry`](/docs/api/registry-cluster). | | **MCP** | `handoff_get_tokens` — foundation tokens slimmed for context use, multi-axis aware (advertises available `axes`; pass `brand`/`scheme` for an axis-resolved result). `handoff_export_design_md` bundles a token brief into the exported `DESIGN.md`. See [MCP → Tool catalog § Tokens](/docs/mcp/tool-catalog#tokens). | Both REST reads are public (no auth), tokens are foundation-level, non-sensitive data every stakeholder needs to see, the same reasoning behind `GET /api/registry/config` being public (see [API → `/api/registry` → public read](/docs/api/registry-cluster#get-apiregistryconfig-is-a-public-read)). # Authentication (/docs/mcp/auth) Handoff's MCP endpoint (`/api/mcp`) and its reference API share one auth layer, `verifyHandoffApiAuth()`. Whether it requires anything at all depends on the run mode. ## Workspace vs. registry [#workspace-vs-registry] * **Workspace mode** (`DATABASE_URL` unset), MCP is **unauthenticated by default**: any local client gets a read-only workspace auth context (admin role, `sync:read reference:read components:read design:read`). Set **`HANDOFF_SYNC_SECRET`** to require a bearer token even in workspace mode. * **Registry mode** (`DATABASE_URL` set), a bearer token is **required**. An unauthenticated request gets `401` with an RFC 9728 `WWW-Authenticate` header pointing a client at protected- resource metadata (see below), rather than a bare rejection. A valid bearer is either: 1. The **legacy shared secret** (`HANDOFF_SYNC_SECRET`), grants every scope, `isLegacySecret: true`. Meant for automation, not interactive clients. 2. An **OAuth 2.1 JWT** with a `scp` claim (space-separated scopes) and an audience of `handoff-api` (or the legacy `handoff-cli-sync`). There are two distinct ways to obtain that JWT, for two different kinds of client. ### 1. CLI / device-code flow (RFC 8628) [#1-cli--device-code-flow-rfc-8628] Used by `handoff-app login`, `mcp-init`, and `mcp-token`: anywhere a human is at a terminal, not a browser-based OAuth redirect. 1. `POST /api/oauth/device` → `{ device_code, user_code, verification_uri, verification_uri_complete }` 2. The CLI opens (or prints) the verification URL; the user approves the request in a browser at `/cli/device`. 3. The CLI polls `POST /api/oauth/token` with `grant_type=urn:ietf:params:oauth:grant-type:device_code` and the `device_code` until the user approves, then gets back `{ access_token, token_type, expires_in }`. 4. That token is used as `Authorization: Bearer ` on every subsequent MCP/API request. ### 2. MCP OAuth connector flow (OAuth 2.1 authorization code + PKCE) [#2-mcp-oauth-connector-flow-oauth-21-authorization-code--pkce] Used by clients that register themselves dynamically and drive a normal OAuth browser redirect, e.g. a **claude.ai Connector** or Claude Desktop's connector UI: 1. The client discovers endpoints via **RFC 8414** Authorization Server Metadata (`/.well-known/oauth-authorization-server`) and, if it first hit `/api/mcp` unauthenticated, the `401`'s `WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource"` header (**RFC 9728**). 2. The client registers itself via **RFC 7591** Dynamic Client Registration (`POST /api/oauth/register`), no manual client setup on Handoff's side. 3. The user is redirected through `/api/oauth/authorize` (response type `code`, `code_challenge_method=S256`) and approves. 4. The client exchanges the code at `POST /api/oauth/token` (`grant_type=authorization_code`, with `code_verifier` per PKCE) for `{ access_token, refresh_token, token_type, expires_in, scope }`. 5. `refresh_token` grants renew access the same way (`grant_type=refresh_token`). ## Scopes [#scopes] The full scope vocabulary (`oauth-scopes.ts`), shared by both flows: | Scope | Covers | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `sync:read` | Checked by `handoff_list_review_queue`. `handoff_sync_status` and `handoff_sync_pull` read sync state but perform no scope check of their own. | | `sync:write` | Push sync changes, write pages/previews/doc pages | | `reference:read` | Design guidelines, brand voice | | `components:read` | Not yet checked by any MCP tool or route — reserved in the vocabulary | | `components:write` | Not yet checked by any MCP tool or route — reserved | | `design:read` | Component reference images | | `design:write` | Not yet checked by any MCP tool or route — reserved (granted to member-role tokens by default) | | `generate:component` | Not yet checked by any MCP tool or route — reserved | | `figma:sync` | Used by the Figma plugin's REST endpoints (`/api/figma-plugin/*`), not by any MCP tool | Role → scope defaults (`scopesForRole`): an **admin** device/connector login receives every scope above; a **member** receives `sync:read reference:read components:read design:read design:write`. Only a handful of tools call an explicit scope check, see the **Scope** column on the [Tool catalog](/docs/mcp/tool-catalog) page. Everything else is gated purely by whether the caller has a valid bearer at all (registry mode) or by workspace mode being open by default. ## Where the token is stored [#where-the-token-is-stored] * **CLI**: written to `.handoff/cli-auth.json` in the workspace by `runCliLogin()`; read back by `getSyncBearerToken()`, reused by `mcp-token` and `mcp-init` without re-prompting until it expires. * **MCP client config**: varies by client, see [Onboarding](/docs/mcp/onboarding) for how `mcp-init` wires the token into `.mcp.json`, and how OAuth connectors handle it automatically. # MCP (/docs/mcp) Handoff exposes a **Model Context Protocol (MCP)** server so an AI agent, Claude, Cursor, Windsurf, or anything else that speaks MCP, can read (and, in scoped places, write) the same design-system data as the UI and REST surfaces. This is the [three-surfaces principle](/docs/architecture#the-three-surfaces-principle) applied to agents: tokens, components, icons, design artifacts, pages, and change history are all reachable as tools, not just as pages or API routes. ## Server identity [#server-identity] The server identifies itself as `handoff`, version `2.0.0`: ```ts new McpServer({ name: 'handoff', version: '2.0.0' }, { capabilities: { tools: {}, resources: {} } }) ``` The `resources` capability is there for [MCP Apps](/docs/mcp/mcp-apps), interactive, UI-bearing tool results, not just plain text/JSON. ## One endpoint, 56 tools, 12 categories [#one-endpoint-56-tools-12-categories] Everything is served from a single route, `/api/mcp`, over a stateless HTTP transport (no SSE, no session affinity), see [Route & transport](/docs/mcp/route-transport) for the details. Fifty-six tools are registered (53 plain tools plus three [MCP Apps](/docs/mcp/mcp-apps)), organized into twelve categories, Context, Components, Tokens, Icons & Logos, Assets, Design Artifacts, Design Workbench, Pages & Compositions, Previews, Documentation, Change Inquiry, and Build & Sync, covered in full on the [Tool catalog](/docs/mcp/tool-catalog) page. A handful of tools aren't in the category map yet and fall into an "Other" bucket on a live registry. ## Where to go next [#where-to-go-next] * **[Route & transport](/docs/mcp/route-transport)**: the endpoint, transport, and how project context gets hydrated on connect. * **[Tool catalog](/docs/mcp/tool-catalog)**: every tool, grouped by category, with its purpose and required scope. * **[Authentication](/docs/mcp/auth)**: workspace vs. registry auth, OAuth 2.1 flows, scopes. * **[Onboarding](/docs/mcp/onboarding)**: `mcp-init` / `mcp-token`, connecting from Claude, Cursor, and OAuth-based connectors. * **[MCP Apps](/docs/mcp/mcp-apps)**: the embedded component-preview UI. * **[Stack guides](/docs/mcp/stack-guides)**: per-framework authoring rules an agent pulls before writing code. Legacy-site equivalent: "Consumption by LLMs" (`using_handoff/consumption_by_llms`), re-homed and substantially expanded here, since MCP didn't exist in v1. # MCP Apps (/docs/mcp/mcp-apps) Most MCP tools return text or JSON for a model to read. **MCP Apps** are the emerging MCP extension (`@modelcontextprotocol/ext-apps`) that lets a tool call instead return an **interactive, HTML-based UI**: rendered by the host in a sandboxed iframe, so a human watching the conversation (or the model itself, via `updateModelContext`) sees something real instead of a JSON blob. ## Handoff's three MCP Apps today [#handoffs-three-mcp-apps-today] Three tools return an interactive surface rather than JSON, each bundled from its own client entry point by `scripts/build-mcp-apps.mjs`: | Tool | App resource | What it renders | | --------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `handoff_preview_component` | `ui://handoff/component-preview` | The actual built component, live, with responsive width controls. | | `handoff_browse_tokens` | `ui://handoff/token-palette` | A palette of color swatches, type specimens and the spacing scale; clicking a token hands it back to the model. | | `handoff_browse_components` | `ui://handoff/component-gallery` | A searchable card grid of the component catalog; select a component to hand it back, or open it in the registry. | ### Component preview [#component-preview] `handoff_preview_component` renders the **actual built component**, live, with responsive width controls: 1. The tool looks up the component and its available preview keys (falling back to `"generic"` or the first available preview). 2. It registers a `ui://handoff/component-preview` resource, an HTML shell with a toolbar and an inner `