handoff-app 2.0.0-9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. package/{Changelog.md → CHANGELOG.md} +85 -1
  2. package/{License.md → LICENSE.md} +1 -1
  3. package/README.md +813 -406
  4. package/RECIPES.md +975 -0
  5. package/UPGRADE.md +57 -9
  6. package/components.json +20 -20
  7. package/config/docs/foundations/colors/guidelines.md +0 -1
  8. package/config/docs/foundations/colors.md +0 -1
  9. package/config/docs/foundations/effects/guidelines.md +0 -1
  10. package/config/docs/foundations/effects/resources.md +0 -1
  11. package/config/docs/foundations/effects.md +0 -1
  12. package/config/docs/foundations/fonts.md +0 -1
  13. package/config/docs/foundations/grid.md +0 -1
  14. package/config/docs/foundations/icon.md +0 -1
  15. package/config/docs/foundations/icons/guidelines.md +0 -1
  16. package/config/docs/foundations/icons.md +0 -1
  17. package/config/docs/foundations/images/guidelines.md +0 -1
  18. package/config/docs/foundations/images/resources.md +0 -1
  19. package/config/docs/foundations/images.md +0 -1
  20. package/config/docs/foundations/logo/resources.md +0 -1
  21. package/config/docs/foundations/logo.md +0 -1
  22. package/config/docs/foundations/typography/guidelines.md +0 -1
  23. package/config/docs/foundations/typography/resources.md +0 -1
  24. package/config/docs/foundations/typography.md +0 -1
  25. package/config/docs/foundations.md +0 -1
  26. package/config/docs/guidelines.md +0 -1
  27. package/config/docs/index.md +0 -1
  28. package/config/docs/system/tokens/foundations/colors.md +0 -4
  29. package/config/docs/system/tokens/foundations/effects.md +0 -4
  30. package/config/docs/system/tokens/foundations/typography.md +0 -2
  31. package/config/docs/system.md +0 -2
  32. package/config/templates/page.md +2 -1
  33. package/config/templates/page.mdx +2 -1
  34. package/dist/ai/connections.d.ts +66 -0
  35. package/dist/ai/connections.js +149 -0
  36. package/dist/ai/define.d.ts +10 -0
  37. package/dist/ai/define.js +13 -0
  38. package/dist/ai/types.d.ts +26 -0
  39. package/dist/ai/types.js +10 -0
  40. package/dist/app-builder/build.d.ts +14 -7
  41. package/dist/app-builder/build.js +195 -66
  42. package/dist/app-builder/client-config.js +3 -2
  43. package/dist/app-builder/paths.d.ts +0 -12
  44. package/dist/app-builder/paths.js +3 -20
  45. package/dist/app-builder/static-export.js +1 -1
  46. package/dist/app-builder/vercel-output.d.ts +1 -1
  47. package/dist/app-builder/vercel-output.js +5 -4
  48. package/dist/cache/build-cache.d.ts +27 -4
  49. package/dist/cache/build-cache.js +114 -47
  50. package/dist/cache/build-inputs.d.ts +13 -0
  51. package/dist/cache/build-inputs.js +19 -0
  52. package/dist/cache/index.d.ts +2 -1
  53. package/dist/cache/index.js +5 -1
  54. package/dist/cli/create.js +1 -2
  55. package/dist/commands/build/index.d.ts +1 -1
  56. package/dist/commands/build/index.js +1 -1
  57. package/dist/commands/utils.d.ts +3 -3
  58. package/dist/config/defaults.js +7 -4
  59. package/dist/config/from-env.d.ts +4 -0
  60. package/dist/config/from-env.js +38 -1
  61. package/dist/config/helpers.js +2 -2
  62. package/dist/config/index.d.ts +0 -1
  63. package/dist/config/index.js +1 -4
  64. package/dist/config/loader.js +26 -2
  65. package/dist/config/resolve-env.d.ts +2 -1
  66. package/dist/config/resolve-env.js +28 -12
  67. package/dist/config/runtime.js +1 -1
  68. package/dist/index.d.ts +12 -7
  69. package/dist/index.js +18 -22
  70. package/dist/nav/index.d.ts +2 -2
  71. package/dist/nav/registry.d.ts +3 -3
  72. package/dist/nav/registry.js +1 -1
  73. package/dist/nav/workspace.d.ts +3 -3
  74. package/dist/nav/workspace.js +1 -1
  75. package/dist/pipeline/figma.d.ts +13 -2
  76. package/dist/pipeline/figma.js +86 -46
  77. package/dist/pipeline/styles.js +1 -1
  78. package/dist/pipeline/validation.d.ts +1 -1
  79. package/dist/pipeline/validation.js +3 -3
  80. package/dist/providers.d.ts +10 -0
  81. package/dist/providers.js +13 -0
  82. package/dist/registry/asset-storage/define.d.ts +5 -5
  83. package/dist/registry/asset-storage/define.js +4 -4
  84. package/dist/registry/asset-storage/resolve.d.ts +9 -19
  85. package/dist/registry/asset-storage/resolve.js +33 -21
  86. package/dist/registry/asset-storage/types.d.ts +37 -17
  87. package/dist/registry/asset-storage/types.js +5 -5
  88. package/dist/registry/auth/ai-keys.d.ts +32 -0
  89. package/dist/registry/auth/ai-keys.js +69 -0
  90. package/dist/registry/auth/crypto.d.ts +17 -0
  91. package/dist/registry/auth/crypto.js +61 -1
  92. package/dist/registry/auth/index.d.ts +2 -1
  93. package/dist/registry/auth/index.js +11 -2
  94. package/dist/registry/checkout/index.d.ts +2 -1
  95. package/dist/registry/checkout/index.js +57 -127
  96. package/dist/registry/client.js +53 -17
  97. package/dist/registry/db/driver.js +3 -3
  98. package/dist/registry/db/schema.d.ts +261 -3
  99. package/dist/registry/db/schema.js +29 -4
  100. package/dist/registry/email/define.d.ts +10 -0
  101. package/dist/registry/email/define.js +13 -0
  102. package/dist/registry/email/providers/smtp.d.ts +10 -0
  103. package/dist/registry/email/providers/smtp.js +73 -0
  104. package/dist/registry/email/resolve.d.ts +28 -0
  105. package/dist/registry/email/resolve.js +74 -0
  106. package/dist/registry/email/types.d.ts +24 -0
  107. package/dist/registry/email/types.js +6 -0
  108. package/dist/registry/path.d.ts +2 -0
  109. package/dist/registry/path.js +11 -1
  110. package/dist/registry/publish/package.js +8 -0
  111. package/dist/registry/transfer.d.ts +4 -0
  112. package/dist/store/registry.d.ts +3 -3
  113. package/dist/store/registry.js +4 -4
  114. package/dist/transformers/preview/component/builder.d.ts +1 -9
  115. package/dist/transformers/preview/component/builder.js +17 -17
  116. package/dist/transformers/preview/component/css.d.ts +2 -1
  117. package/dist/transformers/preview/component/css.js +8 -5
  118. package/dist/transformers/preview/component/html.d.ts +3 -1
  119. package/dist/transformers/preview/component/html.js +6 -2
  120. package/dist/transformers/preview/component/javascript.d.ts +2 -1
  121. package/dist/transformers/preview/component/javascript.js +10 -4
  122. package/dist/transformers/utils/build.d.ts +5 -0
  123. package/dist/transformers/utils/build.js +21 -4
  124. package/dist/types/config.d.ts +129 -18
  125. package/dist/utils/path.d.ts +1 -5
  126. package/dist/utils/path.js +6 -23
  127. package/docs/api.md +6 -4
  128. package/docs/cli.md +275 -54
  129. package/drizzle/0007_add_ai_keys.sql +11 -0
  130. package/drizzle/0008_add_source_dir.sql +2 -0
  131. package/drizzle/meta/0007_snapshot.json +2013 -0
  132. package/drizzle/meta/0008_snapshot.json +2025 -0
  133. package/drizzle/meta/_journal.json +14 -0
  134. package/package.json +19 -8
  135. package/src/ai/README.md +42 -0
  136. package/src/ai/connections.ts +189 -0
  137. package/src/ai/define.ts +15 -0
  138. package/src/ai/types.ts +30 -0
  139. package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
  140. package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
  141. package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
  142. package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
  143. package/src/app/components/AiAssistant/dockState.ts +86 -0
  144. package/src/app/components/AiAssistant/toolLabel.ts +24 -0
  145. package/src/app/components/AiAssistant/useAiChat.ts +147 -0
  146. package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
  147. package/src/app/components/AiAssistant/usePageContext.ts +63 -0
  148. package/src/app/components/Auth/AccountLayout.tsx +58 -19
  149. package/src/app/components/Auth/AuthShell.tsx +27 -33
  150. package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
  151. package/src/app/components/Auth/installStatus.ts +29 -0
  152. package/src/app/components/Component/Preview.tsx +5 -2
  153. package/src/app/components/Foundations/ColorGrid.tsx +13 -14
  154. package/src/app/components/Layout/Header.tsx +21 -15
  155. package/src/app/components/Layout/Main.tsx +2 -2
  156. package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
  157. package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
  158. package/src/app/components/Navigation/MobileNav.tsx +2 -2
  159. package/src/app/components/Navigation/SideNav.tsx +1 -15
  160. package/src/app/components/context/NavProvider.tsx +7 -3
  161. package/src/app/components/ui/attachment.tsx +50 -0
  162. package/src/app/components/ui/bubble.tsx +57 -0
  163. package/src/app/components/ui/button.tsx +3 -1
  164. package/src/app/components/ui/confirm-dialog.tsx +87 -0
  165. package/src/app/components/ui/dialog.tsx +7 -2
  166. package/src/app/components/ui/marker.tsx +56 -0
  167. package/src/app/components/ui/sidebar.tsx +3 -1
  168. package/src/app/components/util/index.ts +3 -12
  169. package/src/app/css/index.css +64 -0
  170. package/src/app/lib/ai/auth.ts +33 -0
  171. package/src/app/lib/ai/context.ts +99 -0
  172. package/src/app/lib/ai/model.ts +79 -0
  173. package/src/app/lib/ai/prompt.ts +36 -0
  174. package/src/app/lib/ai/resolve.ts +97 -0
  175. package/src/app/lib/ai/tools.ts +120 -0
  176. package/src/app/lib/api/methods.ts +13 -0
  177. package/src/app/lib/asset-storage.ts +35 -36
  178. package/src/app/lib/auth/api.ts +4 -12
  179. package/src/app/lib/auth/email.ts +96 -23
  180. package/src/app/lib/docs-api/page-rendering.ts +1 -1
  181. package/src/app/lib/docs-api/registry.ts +2 -3
  182. package/src/app/lib/docs-api/runtime-config.ts +47 -21
  183. package/src/app/lib/mcp/components.ts +523 -0
  184. package/src/app/lib/mcp/page-url.ts +24 -0
  185. package/src/app/lib/mcp/pages.ts +84 -0
  186. package/src/app/lib/mcp/result.ts +33 -0
  187. package/src/app/lib/mcp/server.ts +39 -318
  188. package/src/app/lib/mcp/tokens.ts +334 -0
  189. package/src/app/lib/mcp/tool.ts +19 -0
  190. package/src/app/lib/registry-api/asset-transfer.ts +119 -11
  191. package/src/app/lib/registry-api/errors.ts +2 -0
  192. package/src/app/lib/registry-api/handler.ts +7 -1
  193. package/src/app/lib/registry-api/transfer.ts +28 -6
  194. package/src/app/lib/server-module.ts +34 -0
  195. package/src/app/lib/utils.ts +20 -0
  196. package/src/app/next.config.mjs +26 -24
  197. package/src/app/pages/[...slug]/index.tsx +2 -1
  198. package/src/app/pages/_app.tsx +6 -2
  199. package/src/app/pages/_document.tsx +6 -0
  200. package/src/app/pages/account/ai.tsx +183 -0
  201. package/src/app/pages/account/index.tsx +68 -68
  202. package/src/app/pages/account/tokens.tsx +37 -7
  203. package/src/app/pages/account/users.tsx +75 -15
  204. package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
  205. package/src/app/pages/api/account/ai/keys/index.ts +20 -0
  206. package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
  207. package/src/app/pages/api/admin/users/index.ts +2 -2
  208. package/src/app/pages/api/ai/chat.ts +115 -0
  209. package/src/app/pages/api/ai/connections.ts +23 -0
  210. package/src/app/pages/api/auth/request-reset.ts +14 -16
  211. package/src/app/pages/api/docs/nav.json.ts +1 -1
  212. package/src/app/pages/api/install.ts +3 -3
  213. package/src/app/pages/api/mcp.ts +6 -0
  214. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
  215. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
  216. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
  217. package/src/app/pages/cli/device.tsx +10 -5
  218. package/src/app/pages/foundations/colors.tsx +9 -8
  219. package/src/app/pages/foundations/index.tsx +3 -3
  220. package/src/app/pages/index.tsx +2 -2
  221. package/src/app/pages/install.tsx +7 -2
  222. package/src/app/pages/login.tsx +32 -27
  223. package/src/app/pages/reset-password.tsx +21 -20
  224. package/src/app/pages/system/component/[component]/index.tsx +2 -5
  225. package/src/app/pages/system/index.tsx +11 -11
  226. package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
  227. package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
  228. package/src/app-builder/README.md +2 -1
  229. package/src/app-builder/build.ts +218 -73
  230. package/src/app-builder/client-config.ts +3 -2
  231. package/src/app-builder/paths.ts +3 -22
  232. package/src/app-builder/static-export.ts +1 -1
  233. package/src/app-builder/vercel-output.ts +5 -4
  234. package/src/cache/README.md +4 -1
  235. package/src/cache/build-cache.ts +128 -43
  236. package/src/cache/build-inputs.ts +28 -0
  237. package/src/cache/index.ts +4 -0
  238. package/src/cli/create.ts +1 -2
  239. package/src/commands/build/index.ts +1 -1
  240. package/src/config/README.md +10 -9
  241. package/src/config/defaults.ts +7 -4
  242. package/src/config/from-env.ts +33 -0
  243. package/src/config/helpers.ts +1 -3
  244. package/src/config/index.ts +0 -3
  245. package/src/config/loader.ts +25 -3
  246. package/src/config/resolve-env.ts +28 -15
  247. package/src/config/runtime.ts +1 -1
  248. package/src/index.ts +33 -29
  249. package/src/nav/index.ts +2 -2
  250. package/src/nav/registry.ts +3 -3
  251. package/src/nav/workspace.ts +3 -3
  252. package/src/pipeline/README.md +1 -1
  253. package/src/pipeline/figma.ts +98 -49
  254. package/src/pipeline/styles.ts +1 -1
  255. package/src/pipeline/validation.ts +3 -3
  256. package/src/providers.ts +18 -0
  257. package/src/registry/asset-storage/define.ts +6 -6
  258. package/src/registry/asset-storage/resolve.ts +38 -37
  259. package/src/registry/asset-storage/types.ts +39 -17
  260. package/src/registry/auth/ai-keys.ts +62 -0
  261. package/src/registry/auth/crypto.ts +63 -1
  262. package/src/registry/auth/index.ts +11 -1
  263. package/src/registry/checkout/index.ts +60 -138
  264. package/src/registry/client.ts +60 -20
  265. package/src/registry/db/driver.ts +2 -2
  266. package/src/registry/db/schema.ts +49 -22
  267. package/src/registry/email/define.ts +15 -0
  268. package/src/registry/email/providers/smtp.ts +39 -0
  269. package/src/registry/email/resolve.ts +94 -0
  270. package/src/registry/email/types.ts +28 -0
  271. package/src/registry/path.ts +11 -0
  272. package/src/registry/publish/package.ts +9 -0
  273. package/src/registry/transfer.ts +4 -0
  274. package/src/store/registry.ts +7 -7
  275. package/src/transformers/preview/component/builder.ts +19 -25
  276. package/src/transformers/preview/component/css.ts +9 -4
  277. package/src/transformers/preview/component/html.ts +6 -2
  278. package/src/transformers/preview/component/javascript.ts +14 -4
  279. package/src/transformers/utils/build.ts +16 -3
  280. package/src/types/config.ts +142 -20
  281. package/src/utils/README.md +1 -1
  282. package/src/utils/path.ts +6 -29
  283. package/templates/default/handoff.config.js.tpl +10 -2
  284. package/templates/default/handoff.config.ts.tpl +10 -2
  285. package/templates/default/handoff.config.with-examples.js.tpl +10 -2
  286. package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
  287. package/templates/default/package.json.tpl +2 -2
  288. package/.editorconfig +0 -19
  289. package/.eslintrc.json +0 -4
  290. package/.prettierignore +0 -0
  291. package/.prettierrc.json +0 -8
  292. package/.vscode/settings.json +0 -17
  293. package/dist/config/validator.d.ts +0 -9
  294. package/dist/config/validator.js +0 -24
  295. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
  296. package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
  297. package/src/app/components/old/Header.tsx +0 -89
  298. package/src/app/lib/mcp/shape.ts +0 -596
  299. package/src/app/pages/assets/fonts.tsx +0 -129
  300. package/src/app/pages/assets/index.tsx +0 -91
  301. package/src/app/pages/assets/logos.tsx +0 -75
  302. package/src/config/validator.ts +0 -22
  303. package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
package/README.md CHANGED
@@ -3,80 +3,436 @@
3
3
  <a aria-label="NPM version" href="https://www.npmjs.com/package/handoff-app">
4
4
  <img alt="" src="https://img.shields.io/npm/v/handoff-app?style=for-the-badge&labelColor=000000">
5
5
  </a>
6
- <a aria-label="License" href="https://github.com/convertiv/handoff-app/blob/main/License.md">
6
+ <a aria-label="License" href="https://github.com/convertiv/handoff-app/blob/main/LICENSE.md">
7
7
  <img alt="" src="https://img.shields.io/npm/l/handoff-app?style=for-the-badge&labelColor=000000">
8
8
  </a>
9
9
 
10
- Design tokens, components, and patterns are turned by Handoff into working
11
- documentation and distributable artifacts. A local workspace, a static site,
12
- or a shared PostgreSQL-backed registry can be used.
10
+ Handoff turns design tokens, components, and patterns into working
11
+ documentation and distributable artifacts. You can serve the documentation
12
+ from a local workspace, a static site, or a shared registry with a PostgreSQL
13
+ database.
13
14
 
14
- ## Runtime modes
15
+ > **Recipes:** [RECIPES.md](RECIPES.md) has complete examples for project
16
+ > layouts, catalog items, profiles, registry hosting, asset storage, email, and
17
+ > the AI assistant.
15
18
 
16
- - **Workspace** is the default. Local files are the source of truth and no
17
- database is required.
18
- - **Registry** is a deployed catalog backed by PostgreSQL.
19
- - A **connected workspace** remains in workspace mode but can publish to and
20
- check out from a registry.
19
+ > **Upgrade from version 1.x.x:** read the
20
+ > [migration guide](UPGRADE.md#version-1xx-to-version-2xx) for the breaking
21
+ > changes and the migration steps.
21
22
 
22
- Workspace mode is used by local projects for authoring. A separate
23
- registry-mode application is created automatically by a registry build.
23
+ ## Contents
24
+
25
+ - [How Handoff works](#how-handoff-works)
26
+ - [Requirements](#requirements)
27
+ - [Installation](#installation)
28
+ - [Configuration](#configuration)
29
+ - [Authoring](#authoring)
30
+ - [Running the workspace](#running-the-workspace)
31
+ - [Building](#building)
32
+ - [Registry](#registry)
33
+ - [Connecting a workspace to a registry](#connecting-a-workspace-to-a-registry)
34
+ - [MCP](#mcp)
35
+ - [AI assistant](#ai-assistant)
36
+ - [CLI reference](#cli-reference)
37
+
38
+ ## How Handoff works
39
+
40
+ ### Runtime modes
41
+
42
+ - **Workspace** is the default mode. Local files are the source of truth, and
43
+ no database is necessary. You use it to author content.
44
+ - **Registry** is a deployed catalog with a PostgreSQL database. It stores and
45
+ serves only the content that a workspace publishes to it.
46
+ - A **connected workspace** is a workspace that can publish to a registry and
47
+ check out from it. It is not a third mode.
48
+
49
+ The `runtime.mode` value in the config sets the mode. A registry build sets
50
+ registry mode automatically, so the base config stays in workspace mode.
51
+
52
+ ### Workflow
53
+
54
+ ```text
55
+ Figma ── fetch ──▶ exported/ tokens, CSS, Sass, assets
56
+ catalog, pages ── start ──▶ local documentation at http://localhost:3000
57
+ ── build ──▶ out/static (static site) or out/registry (registry server)
58
+ workspace ── publish ─▶ registry ── checkout ─▶ workspace
59
+ ```
60
+
61
+ The workspace is the only place that builds content. A registry never builds
62
+ and never reads workspace source.
63
+
64
+ ### Terms
65
+
66
+ - **Working directory:** the directory that holds `handoff.config.ts`. Every
67
+ command runs against it.
68
+ - **Catalog item:** one documented UI entry. It has a declaration file, a
69
+ stable id, and previews.
70
+ - **Component:** a catalog item with an implementation (React, Handlebars, or
71
+ CSF).
72
+ - **Pattern:** a catalog item that is a composition of other catalog items.
73
+ - **Foundations:** the tokens and assets that `fetch` gets from Figma.
74
+ - **Page:** a Markdown documentation page under `pages/`.
75
+ - **Profile:** a config file that merges onto the base config for one
76
+ environment.
24
77
 
25
78
  ## Requirements
26
79
 
27
- - Node.js 18.18 or newer; Node.js 20 LTS or newer is recommended
28
- - npm 8 or newer
29
- - A paid Figma account when fetching a Figma library
30
- - PostgreSQL when running a registry
80
+ - Node.js 22 or newer. We recommend Node.js 24 LTS.
81
+ - npm 10 or newer.
82
+ - A paid Figma account, to fetch a Figma library.
83
+ - PostgreSQL, to run a registry.
31
84
 
32
- ## Quick start
85
+ ## Installation
33
86
 
34
- A project is created with the interactive wizard:
87
+ ### New project
88
+
89
+ Create a project with the interactive wizard:
35
90
 
36
91
  ```bash
37
92
  npx handoff-app init
38
93
  cd my-handoff-project
39
94
  ```
40
95
 
41
- The project name, sample content, JavaScript or TypeScript, optional Vercel
42
- setup, and optional Figma credentials are requested by the wizard. Starter
43
- files are created and `handoff-app` is installed locally. Generated npm scripts
44
- are used for project operations so the pinned project version is shared by
45
- local development and CI:
96
+ The wizard asks for the project name, sample content, JavaScript or
97
+ TypeScript, an optional Vercel setup, and optional Figma credentials. It
98
+ creates the starter files and installs `handoff-app` in the project.
99
+
100
+ Use the generated npm scripts. They run the project version of `handoff-app`,
101
+ so local development and CI use the same version:
46
102
 
47
103
  ```bash
48
104
  npm run fetch # optional: fetch Figma foundations
49
105
  npm run start # local server with Handoff file watchers
50
106
  npm run dev # local development server without Handoff file watchers
51
107
  npm run build # static build by default
108
+ npm run publish # publish to a registry
109
+ npm run checkout # check out from a registry
110
+ npm run login # sign in to a registry
111
+ npm run logout # sign out of a registry
52
112
  npm run db:migrate # registry database migrations
53
- npm run validate
54
113
  ```
55
114
 
56
- ## Project layout
115
+ ### Project layout
57
116
 
58
117
  ```text
59
- my-handoff-project/
118
+ <working directory>/
60
119
  ├─ handoff.config.ts
61
120
  ├─ .env
62
121
  ├─ components/
63
122
  ├─ patterns/
64
123
  ├─ pages/
65
124
  ├─ exported/ # fetched tokens and assets; commit this directory
66
- └─ out/ # build output; gitignored
125
+ ├─ public/api/ # generated docs API; gitignored
126
+ ├─ out/ # build output; gitignored
127
+ ├─ .vercel/ # `--package vercel` output; gitignored
128
+ └─ .handoff/ # local Handoff state; gitignored
129
+ ```
130
+
131
+ In a project that `init` creates, the working directory is the repository
132
+ root. `HANDOFF_WORKING_PATH` sets a different working directory.
133
+
134
+ ### Adding Handoff to an existing application
135
+
136
+ Declarations sit next to the components that they document. Thus the catalog
137
+ points into the application source:
138
+
139
+ ```ts
140
+ catalog: { include: ['src/components', 'src/blocks'] },
141
+ ```
142
+
143
+ Two rules apply to every layout.
144
+
145
+ **`handoff-app` must be a dependency of the package that holds the
146
+ `*.handoff.ts` files.** A declaration imports `handoff-app/react`. Handoff
147
+ resolves that import from the directory of the declaration, not from the
148
+ directory of the config. With npm, hoisting usually hides a missing dependency.
149
+ With pnpm, Handoff skips the affected items with
150
+ `Could not resolve "handoff-app/react"`, and the build still exits with code 0.
151
+ In a workspace-package layout, the application package and the `handoff/`
152
+ package must both declare `handoff-app`.
153
+
154
+ **Two `tsconfig.json` files do different jobs:**
155
+
156
+ - The config nearest to the declarations type checks them. An editor reads
157
+ only the nearest config. A config in a different directory that includes
158
+ `src/` does not count.
159
+ - The config in the working directory makes the property tables. Without it,
160
+ Handoff shows `TypeScript config not found`, and every table is empty.
161
+
162
+ If the working directory is not the project root, you need both files.
163
+
164
+ Three layouts work. Each keeps one dependency tree, so the catalog renders the
165
+ real components with the same React instance. Handoff does not support a
166
+ separate install with its own `node_modules` outside that tree.
167
+
168
+ | Layout | Working directory | Use when |
169
+ | --- | --- | --- |
170
+ | Subdirectory (recommended) | `handoff/`, set by `HANDOFF_WORKING_PATH` | The application must not change |
171
+ | Project root | The project root | The bundler can move its `public/` directory (not Next.js) |
172
+ | Workspace package | `handoff/`, with its own `package.json` | The repository is already a workspace |
173
+
174
+ In the project-root layout, Handoff writes the generated documentation API to
175
+ `public/api`. Thus a bundler that serves `public/` copies it into the
176
+ production build of the application. Move the bundler to a different
177
+ directory. A Next.js application cannot move its `public` directory, so it
178
+ cannot use this layout.
179
+
180
+ With pnpm, `build --target registry` cannot follow the pnpm symlinks. Set
181
+ `nodeLinker: hoisted` in `pnpm-workspace.yaml`. The other builds work with the
182
+ default pnpm layout.
183
+
184
+ To add Handoff in the recommended subdirectory layout:
185
+
186
+ 1. Install `handoff-app` in the application package:
187
+
188
+ ```bash
189
+ npm install handoff-app
190
+ ```
191
+
192
+ 2. Create `handoff/handoff.config.ts`. Point the catalog at the application
193
+ source, and load the global stylesheet of the application into the
194
+ previews:
195
+
196
+ ```ts
197
+ import { defineConfig } from 'handoff-app';
198
+
199
+ export default defineConfig({
200
+ app: { title: 'Acme Design System' },
201
+ catalog: { include: ['../src/components'] },
202
+ entries: { scss: '../src/styles/main.scss' },
203
+ });
204
+ ```
205
+
206
+ Paths are relative to the working directory. `npx handoff-app eject:config`
207
+ writes the full default config instead.
208
+
209
+ 3. Create `handoff/tsconfig.json`, so that Handoff can make the property
210
+ tables:
211
+
212
+ ```json
213
+ { "extends": "../tsconfig.json", "include": ["../src", "handoff.config.ts"] }
214
+ ```
215
+
216
+ 4. Add the scripts to the `package.json` of the application:
217
+
218
+ ```json
219
+ {
220
+ "scripts": {
221
+ "handoff:dev": "HANDOFF_WORKING_PATH=handoff handoff-app start",
222
+ "handoff:build": "HANDOFF_WORKING_PATH=handoff handoff-app build"
223
+ }
224
+ }
225
+ ```
226
+
227
+ 5. Write a declaration next to a component, for example
228
+ `src/components/button/button.handoff.ts`. The
229
+ [Catalog items](#catalog-items) section shows the format.
230
+
231
+ 6. Start the documentation:
232
+
233
+ ```bash
234
+ npm run handoff:dev
235
+ ```
236
+
237
+ 7. Add `handoff/public/api`, `handoff/out`, `handoff/.handoff`, and
238
+ `handoff/.vercel` to `.gitignore`. Commit `handoff/exported` if you fetch
239
+ from Figma.
240
+
241
+ > **Recipes:** [Subdirectory](RECIPES.md#handoff-in-a-subdirectory) ·
242
+ > [Project root](RECIPES.md#handoff-in-the-project-root) ·
243
+ > [Workspace package](RECIPES.md#handoff-as-a-workspace-package) ·
244
+ > [pnpm](RECIPES.md#pnpm) · [npm scripts on Windows](RECIPES.md#npm-scripts-on-windows)
245
+
246
+ ## Configuration
247
+
248
+ ### Config file
249
+
250
+ Handoff reads `handoff.config.ts`, `.js`, `.cjs`, or `.json`, in that order.
251
+ `-c, --config <file>` loads one specific file instead. `defineConfig` gives
252
+ typed authoring:
253
+
254
+ ```ts
255
+ // handoff.config.ts
256
+ import { defineConfig, fromEnv } from 'handoff-app';
257
+
258
+ export default defineConfig({
259
+ app: { title: 'Acme Design System' },
260
+ catalog: { include: ['components', 'patterns'] },
261
+ runtime: {
262
+ mode: 'workspace',
263
+ workspace: { declarationFormat: 'ts' },
264
+ registry: {
265
+ database: { url: fromEnv('DATABASE_URL'), driver: 'pg' },
266
+ },
267
+ },
268
+ });
67
269
  ```
68
270
 
69
- ## Catalog items
271
+ Values merge onto the defaults. Plain objects merge recursively. Arrays,
272
+ scalars, `null`, and functions replace the default.
273
+
274
+ ### Main settings
275
+
276
+ | Setting | Purpose |
277
+ | --- | --- |
278
+ | `app.title`, `app.client` | Name of the documentation site and of the client |
279
+ | `app.theme` | Theme of the documentation app. `npx handoff-app eject:theme` copies it so that you can change it. |
280
+ | `app.basePath` | URL path of the site when you serve it under a sub-path, for example `/design-system` |
281
+ | `app.breakpoints` | Preview widths. A declared block merges onto the defaults. |
282
+ | `app.ports` | Workspace server ports, `{ app, websocket }`. The defaults are 3000 and 3001. |
283
+ | `catalog.include` | Catalog directories. See [Registration](#registration). |
284
+ | `entries.scss`, `entries.js` | Global stylesheet and script that Handoff builds and loads into every preview |
285
+ | `integrations.figma` | Figma file and token. See [Figma foundations](#figma-foundations). |
286
+ | `exportsOutputDirectory` | Directory for fetched foundations. The default is `exported`. |
287
+ | `sitesOutputDirectory` | Directory for build output. The default is `out`. |
288
+ | `runtime.mode` | `workspace` (default) or `registry` |
289
+ | `runtime.workspace.declarationFormat` | Format of the declarations that `checkout` writes: `ts`, `js`, or `cjs` |
290
+ | `runtime.registryConnection` | Registry URL and token of a connected workspace |
291
+ | `runtime.registry` | Registry database, asset storage, and email. See [Registry](#registry). |
292
+ | `runtime.mcp` | MCP endpoint on or off. See [MCP](#mcp). |
293
+ | `runtime.ai` | AI assistant. See [AI assistant](#ai-assistant). |
294
+ | `hooks` | Build hooks, documented in [docs/api.md](docs/api.md) |
295
+
296
+ The `Config` type documents every setting. Several settings read an environment
297
+ variable by default. [Environment variables](#environment-variables) lists them.
298
+
299
+ ### Build-time and runtime values
300
+
301
+ `fromEnv('NAME')` refers to an environment variable. It does not copy the
302
+ value into the config.
303
+
304
+ - A literal value is baked into the build.
305
+ - For a `fromEnv()` value, the build keeps only the variable name. A deployed
306
+ registry reads the value at request time. Thus you can change it without a
307
+ rebuild.
308
+ - Secrets must use `fromEnv()`.
309
+
310
+ Some settings change the shape of the build. These settings are baked, so a
311
+ change to a deployed registry needs a rebuild:
312
+
313
+ - `runtime.mode`
314
+ - `runtime.mcp`
315
+ - `runtime.ai.enabled`
316
+ - The database driver
317
+ - The asset storage provider and the email provider
318
+ - Every custom server module
319
+
320
+ ### Profiles
321
+
322
+ A profile is a config file that merges onto the base config. It holds only what
323
+ changes between environments. Thus a project keeps one shared config, not a
324
+ second full copy.
325
+
326
+ - The base config is `handoff.config.ts`, `.js`, `.cjs`, or `.json`. When no
327
+ profile is selected, Handoff loads only this file.
328
+ - A profile file is `handoff.config.<profile>.*`, with the same four
329
+ extensions.
330
+ - Select a profile with `--profile <name>` or with `HANDOFF_PROFILE`.
331
+ `--profile` has priority over `HANDOFF_PROFILE`.
332
+ - Profile names are not predefined. A name can contain lowercase letters,
333
+ numbers, and hyphens.
334
+ - A selected profile must exist. If no `handoff.config.<name>.*` file exists,
335
+ the command stops with an error.
336
+ - `login`, `logout`, `publish`, and `checkout` also accept a profile that has
337
+ only a saved registry login. For these commands, the profile selects the
338
+ registry. Every other command needs the config file.
339
+ - Layers apply in this order: defaults, base config, profile, then
340
+ programmatic config. The [merge rules](#config-file) apply to each layer.
341
+ - A selected profile also reads `.env.<profile>` on top of `.env`, if that file
342
+ exists. Handoff reads both files from the directory where the command runs. A
343
+ variable that is already in the environment has priority over both files.
344
+
345
+ ```bash
346
+ npx handoff-app build --target registry --profile registry
347
+ ```
348
+
349
+ A profile is a good place for the registry build settings: the database
350
+ driver, the database variable name, the asset storage, the email provider, and
351
+ MCP. `build --target registry` always builds a registry-mode application, so
352
+ the profile does not set `runtime.mode`. A hosting provider or a CI job selects
353
+ the profile with `HANDOFF_PROFILE`.
354
+
355
+ `defineConfig` types a profile and a base config the same way. The generated
356
+ `.gitignore` lists `handoff.config.local.*`. Thus `local` is the usual name for
357
+ a profile that stays on one machine.
358
+
359
+ Arrays replace. Thus a profile that declares `catalog.include` also controls
360
+ what `make:component` and `checkout` register.
361
+
362
+ Handoff reads `.env.<profile>` once at startup and does not watch it. It cannot
363
+ set `HANDOFF_WORKING_PATH`, because Handoff finds the working path before it
364
+ knows the profile.
365
+
366
+ > **Recipes:** [Local profile](RECIPES.md#local-profile) ·
367
+ > [Registry build profile](RECIPES.md#registry-build-profile) ·
368
+ > [One workspace, several registries](RECIPES.md#one-workspace-several-registries)
369
+
370
+ ### Environment variables
371
+
372
+ Handoff reads two kinds of variables:
373
+
374
+ - **Fixed variables.** Handoff always reads these names.
375
+ - **Setting defaults.** Each one is the default `fromEnv()` name of a setting.
376
+ To use a different name, set the setting to `fromEnv('YOUR_NAME')`.
377
+
378
+ This section does not list the variables that you name in the config. Examples
379
+ are the keys of a custom asset storage provider or of an AI connection. A
380
+ standalone registry build writes `out/registry/README.md`, which lists every
381
+ variable that the build needs, with the names from your config.
382
+
383
+ **Fixed variables**
384
+
385
+ | Variable | Purpose |
386
+ | --- | --- |
387
+ | `HANDOFF_PROFILE` | Config profile merged onto the base config |
388
+ | `HANDOFF_WORKING_PATH` | Directory that holds `handoff.config.ts`. The default is the current directory. |
389
+ | `HANDOFF_LOG_LEVEL` | `debug`, `info`, `warn`, `error`, or `silent`. The default is `info`. |
390
+ | `HANDOFF_LOG_SCOPES` | Comma-separated log sources: `handoff`, `vite`, `next`. The default is all three. |
391
+ | `HANDOFF_LOGIN_NO_BROWSER` | Set to `true` to stop `login` from opening a browser, the same as `--no-browser` |
392
+ | `HANDOFF_CREATE_ASSETS_ZIP_FILES` | Set to `false` to skip the icon and logo zip files in `fetch` |
393
+ | `HANDOFF_SYNC_SECRET` | Optional deployment-wide credential with read and write access. The registry accepts it. A workspace uses it only when it has no token and no login. |
394
+ | `AUTH_SECRET` | Registry session-signing secret, at least 32 characters. Required by a registry. |
395
+ | `AUTH_URL` | Canonical public registry URL, with the base path. Required by a registry. |
396
+ | `PORT` | Standalone registry server port |
397
+ | `HOSTNAME` | Standalone registry server bind hostname |
398
+ | `HANDOFF_AI_KEY_SECRET` | Encrypts the AI provider keys of readers, at least 32 characters |
399
+ | `HANDOFF_AI_CONNECTIONS` | JSON array of AI connections, merged over the built-in list by `id` |
400
+
401
+ **Setting defaults**
402
+
403
+ | Variable | Setting | Purpose |
404
+ | --- | --- | --- |
405
+ | `HANDOFF_FIGMA_PROJECT_ID` | `integrations.figma.projectId` | Figma file ID that `fetch` uses |
406
+ | `HANDOFF_DEV_ACCESS_TOKEN` | `integrations.figma.accessToken` | Figma personal access token that `fetch` uses |
407
+ | `HANDOFF_REGISTRY_URL` | `runtime.registryConnection.url` | Registry URL of a connected workspace |
408
+ | `HANDOFF_REGISTRY_ACCESS_TOKEN` | `runtime.registryConnection.accessToken` | Registry access token of a connected workspace |
409
+ | `DATABASE_URL` | `runtime.registry.database.url` | PostgreSQL connection string. Required by a registry. |
410
+ | `HANDOFF_APP_PORT` | `app.ports.app` | Workspace documentation server port. The default is 3000. |
411
+ | `HANDOFF_WEBSOCKET_PORT` | `app.ports.websocket` | Workspace live-reload server port. The default is 3001. |
412
+ | `HANDOFF_OUTPUT_DIR` | `exportsOutputDirectory` | Fetched output directory. The default is `exported`. |
413
+ | `HANDOFF_SITES_DIR` | `sitesOutputDirectory` | Build output directory. The default is `out`. |
414
+
415
+ Keep the name `HANDOFF_OUTPUT_DIR`. The documentation app also reads this
416
+ variable directly.
417
+
418
+ ## Authoring
419
+
420
+ ### Catalog items
70
421
 
71
- Every documented UI entry is a catalog item. Each item declares either an implementation or a composition of other items.
72
- An implementation names its renderer. `defineCatalogItem` comes from the module for that renderer.
73
- The declaration supplies stable identity, documentation metadata, source entries, and previews.
422
+ Every documented UI entry is a catalog item. An item declares one of these:
74
423
 
75
- ### React
424
+ - An `implementation`, which names a renderer: React, Handlebars, or CSF.
425
+ The registry stores the item as a component.
426
+ - A `composition` of other items. The registry stores the item as a pattern.
76
427
 
77
- Handoff finds the implementation file from its import, so you do not need to repeat the path, and
78
- builds the item's CSS from the stylesheets that import chain pulls in, descendants included.
79
- Previews are named exports. `Preview<typeof item>` gives them the implementation's argument type.
428
+ An item never declares both. `defineCatalogItem` comes from the module of the
429
+ renderer. The declaration gives the stable id, the documentation metadata, the
430
+ source entries, and the previews.
431
+
432
+ **React.** Handoff finds the implementation file from its import, so you do not
433
+ repeat the path. Handoff builds the CSS of the item from the stylesheets that
434
+ the import chain loads, descendants included. Previews are named exports.
435
+ `Preview<typeof item>` gives them the argument type of the implementation.
80
436
 
81
437
  ```tsx
82
438
  // components/example/Component.tsx
@@ -112,88 +468,23 @@ export const Default = {
112
468
  } satisfies ComponentPreview;
113
469
  ```
114
470
 
115
- The export name is the preview name. Use `name` to set a different display title.
471
+ The export name is the preview name. `name` sets a different display title.
472
+ Handoff serializes the arguments. Thus a preview that needs an icon, a
473
+ callback, JSX children, or its own state declares a `render` function. A
474
+ declaration that holds JSX must use the `.handoff.tsx` extension.
116
475
 
117
- Arguments are serialized, so a preview that needs an icon, a callback, JSX children, or its own
118
- state declares `render` instead. It renders as a component, so a capitalized name lets it use hooks, and the item keeps
119
- documenting the implementation rather than a wrapper. Name a declaration that holds JSX
120
- `example.handoff.tsx`.
476
+ **Handlebars.** The implementation is the path to the template:
477
+ `implementation: './Badge.hbs'`, from `handoff-app/handlebars`.
121
478
 
122
- ```tsx
123
- export const Interactive = {
124
- render: function Interactive() {
125
- const [label, setLabel] = useState('Example');
126
- return <Component label={label} onSelect={() => setLabel('Selected')} />;
127
- },
128
- } satisfies ComponentPreview;
129
- ```
130
-
131
- ### Handlebars
479
+ **CSF.** A React item uses an existing story file with
480
+ `implementation: fromCSF('./Card.stories.tsx')`. The story file does not
481
+ change, and its named exports are the previews. The server renders a CSF
482
+ preview, and the browser does not hydrate it. The browser hydrates a direct
483
+ React implementation.
132
484
 
133
- The implementation is the path to the template. Arguments are `Record<string, unknown>` by default;
134
- an explicit argument type is supplied through `defineCatalogItem<BadgeArgs>`.
135
-
136
- ```ts
137
- // components/badge/badge.handoff.ts
138
- import { defineCatalogItem, type Preview } from 'handoff-app/handlebars';
139
-
140
- const item = defineCatalogItem({
141
- id: 'badge',
142
- name: 'Badge',
143
- implementation: './Badge.hbs',
144
- });
145
-
146
- export default item;
147
-
148
- type BadgePreview = Preview<typeof item>;
149
-
150
- export const Primary = {
151
- args: { variant: 'primary', children: 'New' },
152
- } satisfies BadgePreview;
153
- ```
154
-
155
- ### CSF
156
-
157
- CSF is a source format. A story file uses the renderer from the module that declares it. A React item references an existing CSF file with `fromCSF`.
158
- The CSF file stays unchanged and defines its previews through named exports. Each story's `args` merge with and override `meta.args`.
159
- Handoff preserves story names, `argTypes`, and `render` functions.
160
- It finds the React component through `meta.component`, then documents and publishes it with the story file.
161
-
162
- ```ts
163
- // components/card/card.handoff.ts
164
- import { defineCatalogItem, fromCSF } from 'handoff-app/react';
165
-
166
- export default defineCatalogItem({
167
- id: 'card',
168
- name: 'Card',
169
- implementation: fromCSF('./Card.stories.tsx'),
170
- });
171
- ```
172
-
173
- `Meta` and `StoryObj` are exported from `handoff-app/react`, so a story file type-checks without a
174
- Storybook dependency. A project that already has Storybook keeps using its own types.
175
-
176
- ```tsx
177
- // components/card/Card.stories.tsx
178
- import { type Meta, type StoryObj } from 'handoff-app/react';
179
- import Card from './Card';
180
-
181
- const meta = { component: Card } satisfies Meta<typeof Card>;
182
- export default meta;
183
-
184
- export const Primary: StoryObj<typeof meta> = {
185
- args: { title: 'Example' },
186
- };
187
- ```
188
-
189
- A CSF preview is rendered on the server and is not hydrated. A direct React implementation is
190
- hydrated in the browser.
191
-
192
- ### Compositions
193
-
194
- A composition references other items by stable id. A named preview can be selected and its arguments
195
- can be overridden by each reference. A composition needs no renderer, so it is declared with
196
- `defineCatalogItem` from `handoff-app/pattern`.
485
+ **Compositions.** A composition references other items by stable id. Each
486
+ reference can select a named preview and override its arguments. A composition
487
+ needs no renderer, so it uses `defineCatalogItem` from `handoff-app/pattern`:
197
488
 
198
489
  ```ts
199
490
  // patterns/example/example.handoff.ts
@@ -211,233 +502,353 @@ export default defineCatalogItem({
211
502
  });
212
503
  ```
213
504
 
214
- An item declares `implementation` or `composition`, never both. Terms such as atom, element, and
215
- block stay optional classification metadata on `type` and `categories`.
216
-
217
- The `id` addresses the item everywhere: its artifacts, its documentation URL, and its publish and
218
- checkout commands. Components and patterns share one namespace, so each id must be unique across
219
- both.
505
+ **Ids.** The `id` addresses the item everywhere: its artifacts, its
506
+ documentation URL, and the `publish` and `checkout` commands. Components and
507
+ patterns share one namespace, so each id must be unique across both. If two
508
+ declarations use one id, Handoff keeps the first, skips the second, and names
509
+ both files in a warning. `publish` does not run until each id is unique.
220
510
 
221
- If two declarations claim one id, Handoff keeps the first and skips the second. The warning names
222
- both files. `publish` refuses to run until each id is unique.
511
+ Terms such as atom, element, and block are optional classification metadata on
512
+ `type` and `categories`.
223
513
 
514
+ > **Recipes:** [React preview with a render function](RECIPES.md#react-preview-with-a-render-function) ·
515
+ > [Handlebars item](RECIPES.md#handlebars-item) · [CSF stories](RECIPES.md#csf-stories) ·
516
+ > [Composition with preview overrides](RECIPES.md#composition-with-preview-overrides)
224
517
 
225
518
  ### Registration
226
519
 
227
- Catalog directories are registered in one place. A path is either an item directory or a collection
228
- directory whose subdirectories are each treated as an item.
520
+ `catalog.include` in the config registers the catalog directories. Each path is
521
+ one of these:
229
522
 
230
- ```ts
231
- // handoff.config.ts
232
- import { defineConfig } from 'handoff-app';
523
+ - An item directory.
524
+ - A collection directory. Each subdirectory is an item.
233
525
 
234
- export default defineConfig({
235
- catalog: {
236
- include: ['components', 'patterns'],
237
- },
238
- runtime: {
239
- workspace: {
240
- declarationFormat: 'ts',
241
- },
242
- },
243
- });
526
+ ```ts
527
+ catalog: {
528
+ include: ['components', 'patterns'],
529
+ },
244
530
  ```
245
531
 
246
- ### Upgrade from version 1.x.x to version 2.x.x
532
+ `npx handoff-app make:component <name>` creates a Handlebars item and adds its
533
+ directory to `catalog.include` when necessary.
247
534
 
248
- See the [version 1.x.x to version 2.x.x migration guide](UPGRADE.md#version-1xx-to-version-2xx)
249
- for breaking changes, migration steps, and upgrade verification.
535
+ ### Pages
250
536
 
251
- Custom documentation pages are Markdown files under `pages/`. Their relative
252
- paths become their routes and registry IDs.
537
+ Custom documentation pages are Markdown files under `pages/`. The relative
538
+ path of a page is its route and its registry id.
539
+ `npx handoff-app make:page <name> [parent]` creates a page.
540
+ `npx handoff-app eject:pages` copies the default pages so that you can change
541
+ them.
253
542
 
254
- ## Figma foundations
543
+ ### Figma foundations
255
544
 
256
- Figma integration is optional. When Figma credentials are entered during
257
- `init`, they are written to `.env` by the wizard and the fetch command can be
258
- run directly.
545
+ The Figma integration is optional. If you enter Figma credentials during
546
+ `init`, the wizard writes them to `.env`, and `fetch` is ready to run.
259
547
 
260
- When that step is skipped or the Figma source must be changed, these values can
261
- be added or updated in `.env`:
548
+ To add or change the Figma source, set these values in `.env`:
262
549
 
263
- ```bash
550
+ ```dotenv
264
551
  HANDOFF_FIGMA_PROJECT_ID=figma-file-id
265
552
  HANDOFF_DEV_ACCESS_TOKEN=figma-personal-access-token
266
553
  ```
267
554
 
268
- The `file_content:read` and `library_content:read` scopes must be granted to the
269
- personal access token. The configured library is fetched with:
555
+ The personal access token must have the `file_content:read` and
556
+ `library_content:read` scopes. Fetch the library:
270
557
 
271
558
  ```bash
272
559
  npm run fetch
273
560
  ```
274
561
 
275
- After design changes, the Figma library should be republished and the command
276
- should be run again so the latest foundations are pulled.
562
+ `fetch` writes tokens, CSS, Sass, and the available assets to `exported/`.
563
+ Commit `exported/` so that local builds and CI builds use the same inputs. To
564
+ make sure that the fetch worked, look for `exported/tokens.json`. An empty
565
+ foundation page does not prove that the fetch worked.
566
+
567
+ If a value is empty, `fetch` asks for it in the terminal. It can save the value
568
+ to `.env`, or to `.env.<profile>` when a profile is selected. Without a
569
+ terminal, for example in CI, `fetch` stops with an error that names the
570
+ missing variable.
571
+
572
+ After a design change, publish the Figma library again. Then run `fetch` again
573
+ to get the latest foundations.
574
+
575
+ These variable names are the defaults of `integrations.figma`. Set the block
576
+ only to use different variable names, to commit the file ID, or to change the
577
+ file for a profile. The access token accepts only an environment reference:
277
578
 
278
- Generated tokens, CSS, Sass, and available assets are written below
279
- `exported/<projectId>`. The `exported/` directory should be committed so the
280
- same inputs are used by local and CI builds. A successful fetch can be verified
281
- by checking for generated token data such as `exported/<projectId>/tokens.json`;
282
- an empty foundation route does not prove that the fetch succeeded.
579
+ ```ts
580
+ integrations: {
581
+ figma: {
582
+ projectId: fromEnv('HANDOFF_FIGMA_PROJECT_ID', { default: null }),
583
+ accessToken: fromEnv('HANDOFF_DEV_ACCESS_TOKEN', { default: null }),
584
+ },
585
+ },
586
+ ```
283
587
 
284
- ## Workspace
588
+ ## Running the workspace
285
589
 
286
- Components, patterns, pages, styles, and configuration are authored as local
287
- workspace files. For day-to-day authoring, the documentation server and file
288
- watchers are started with:
590
+ Start the documentation server and the Handoff file watchers:
289
591
 
290
592
  ```bash
291
593
  npm run start
292
594
  ```
293
595
 
294
- Changes are rebuilt and reflected in the documentation while the command is
295
- running. When Handoff file watchers are not required, the development server
296
- can be started with `npm run dev` instead.
596
+ While the command runs, Handoff rebuilds changed files and updates the
597
+ documentation. If you do not need the file watchers, run `npm run dev`
598
+ instead.
297
599
 
298
- The following should be visible at http://localhost:3000:
600
+ Open http://localhost:3000 and make sure that:
299
601
 
300
- - the app reports `Workspace` as the active runtime mode;
301
- - components appear in the component list;
302
- - component detail pages render every configured preview;
303
- - patterns render their referenced components; and
304
- - foundation pages contain the fetched values.
602
+ - The app shows `Workspace` as the active runtime mode.
603
+ - The component list shows your components.
604
+ - Each component page renders all of its previews.
605
+ - Each pattern renders the components that it references.
606
+ - The foundation pages show the fetched values.
305
607
 
306
- ## Build outputs
608
+ ## Building
307
609
 
308
- A static documentation site is built with:
610
+ **Static site.** Build the documentation as a static site:
309
611
 
310
612
  ```bash
311
613
  npm run build
312
614
  ```
313
615
 
314
- Static output is written below `out/<projectId>` and can be served by any
315
- static file server or CDN.
616
+ The output goes to `out/static`. A static file server or a CDN can serve it. A
617
+ static site has no API routes. Thus it has no MCP endpoint and no AI
618
+ assistant.
316
619
 
317
- A standalone registry application is built with:
620
+ **Build cache.** Handoff does not build a component again if its files did not
621
+ change since the last build. The cache in `.handoff/.cache/` records the files
622
+ of each component and every file that its last build read, Sass partials
623
+ included. The lockfile covers files under `node_modules`. The cache does not
624
+ track assets that a stylesheet loads through `url()`. Build every component
625
+ again:
318
626
 
319
627
  ```bash
320
- npm run build -- --target registry
628
+ npm run build -- --force
321
629
  ```
322
630
 
323
- The generated application is automatically configured for registry mode by the
324
- command. The database-backed Next.js bundle is written to `out/registry`, and
325
- workspace source is not compiled or served. A Vercel Build Output bundle can be
326
- produced for either target by adding `--package vercel`.
631
+ **Registry server.** Build a standalone registry application:
327
632
 
328
- ## Registry setup
633
+ ```bash
634
+ npm run build -- --target registry
635
+ ```
329
636
 
330
- ### 1. Database migrations
637
+ The command sets registry mode in the generated application. It writes the
638
+ Next.js server bundle to `out/registry`. It does not compile or serve the
639
+ workspace source. The [Registry](#registry) section explains how to deploy it.
331
640
 
332
- Migrations are run from the source workspace or CI checkout, where the project
333
- dependency and configuration are available:
641
+ **Vercel.** Add `--package vercel` to either target to make a Vercel Build
642
+ Output bundle in `.vercel/output`:
334
643
 
335
644
  ```bash
336
- DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public" \
337
- npm run db:migrate
645
+ npm run build -- --target registry --package vercel
338
646
  ```
339
647
 
340
- Migrations are a controlled release step and are separate from the build,
341
- server startup, and browser installer.
648
+ ## Registry
342
649
 
343
- ### 2. Registry runtime
650
+ A registry is a deployed catalog with a PostgreSQL database. It serves only
651
+ the records that a workspace publishes. It never reads the workspace
652
+ directly.
344
653
 
345
- The generated registry bundle uses these environment variables:
654
+ ### Deploy a registry
346
655
 
347
- ```dotenv
348
- PORT=4000
349
- HOSTNAME=0.0.0.0
350
- DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public"
351
- AUTH_SECRET="replace-with-at-least-32-random-characters"
352
- AUTH_URL="http://localhost:4000"
353
- ```
656
+ 1. Apply the database migrations from the workspace or a CI checkout, where the
657
+ project config is available:
354
658
 
355
- `DATABASE_URL`, `AUTH_SECRET`, and `AUTH_URL` are required. The standalone
356
- server also reads `PORT` and `HOSTNAME`. The generated entrypoint is
357
- `out/registry/server.js`; how it is started and hosted depends on the deployment
358
- environment.
659
+ ```bash
660
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public" \
661
+ npm run db:migrate
662
+ ```
359
663
 
360
- ### Vercel and Neon
664
+ Migrations are a controlled release step. The build, the server start, and
665
+ the installer never run them. Run `db:migrate` again each time you upgrade
666
+ `handoff-app`, before you deploy the new registry build.
361
667
 
362
- Vercel packaging and the Neon PostgreSQL driver are supported independently.
363
- Either integration can be used without the other.
668
+ 2. Select the [database driver](#database), the [asset storage](#asset-storage),
669
+ and the [email provider](#email). The build bakes these choices, so set them
670
+ before you build. The defaults work without more configuration.
364
671
 
365
- #### Vercel packaging
672
+ 3. Build the registry:
366
673
 
367
- A registry bundle that follows the Vercel Build Output API is produced with:
674
+ ```bash
675
+ npm run build -- --target registry
676
+ ```
368
677
 
369
- ```bash
370
- npm run build -- --target registry --package vercel
371
- ```
678
+ 4. Start `out/registry/server.js` with these environment variables:
679
+
680
+ ```dotenv
681
+ PORT=4000
682
+ HOSTNAME=0.0.0.0
683
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public"
684
+ AUTH_SECRET="replace-with-at-least-32-random-characters"
685
+ AUTH_URL="http://localhost:4000"
686
+ ```
372
687
 
373
- The generated deployment output is written to `.vercel/output`. The registry
374
- runtime variables listed above are supplied through the deployment environment.
688
+ `DATABASE_URL`, `AUTH_SECRET`, and `AUTH_URL` are required. The standalone
689
+ server also reads `PORT` and `HOSTNAME`. For Vercel, build with
690
+ `--package vercel` and set these variables in the deployment environment.
375
691
 
376
- #### Neon PostgreSQL
692
+ 5. Open http://localhost:4000/install and create the first administrator. The
693
+ installer examines the deployment, but it never runs migrations.
377
694
 
378
- Import `fromEnv` from `handoff-app`. Select the Neon connection driver in `handoff.config.ts`:
695
+ CAUTION: Complete the installation immediately after the deployment. The first
696
+ visitor can claim a registry that is not installed.
697
+
698
+ > **Recipes:** [Local registry](RECIPES.md#local-registry) ·
699
+ > [Vercel and Neon](RECIPES.md#vercel-and-neon)
700
+
701
+ ### Database
702
+
703
+ PostgreSQL is the supported database. `driver` selects how the registry
704
+ connects. The default is `pg`. Use `neon` for Neon PostgreSQL:
379
705
 
380
706
  ```ts
381
707
  runtime: {
382
708
  registry: {
383
- databaseUrl: fromEnv('DATABASE_URL'),
384
709
  database: {
710
+ url: fromEnv('DATABASE_URL'),
385
711
  driver: 'neon',
386
712
  },
387
713
  },
388
714
  },
389
715
  ```
390
716
 
391
- A Neon PostgreSQL connection string is supplied through `DATABASE_URL`. The
392
- same explicit database migration step is required before deployment.
717
+ Both drivers use the same schema and the same `db:migrate` step.
718
+
719
+ ### Asset storage
393
720
 
394
- #### Optional Vercel Blob storage
721
+ By default, the registry stores asset blobs in PostgreSQL. Each blob can be at
722
+ most 4 MB. `assetStorage.maxInlineBytes` changes this limit.
395
723
 
396
- Registry assets are stored in PostgreSQL by default. Vercel Blob can be selected
397
- when external asset storage is required:
724
+ A [custom server module](#custom-server-modules) can store blobs in a
725
+ different service:
398
726
 
399
727
  ```ts
400
728
  runtime: {
401
729
  registry: {
402
730
  assetStorage: {
403
- adapter: 'vercel-blob',
731
+ provider: 'custom',
732
+ module: './server/storage/s3.mjs',
733
+ options: { bucket: 'handoff-assets', secretKey: fromEnv('S3_SECRET_KEY') },
404
734
  },
405
735
  },
406
736
  },
407
737
  ```
408
738
 
409
- The associated `BLOB_READ_WRITE_TOKEN` is supplied through the deployment
410
- environment.
739
+ The module default-exports a `defineStorageProvider()` result with these
740
+ methods:
741
+
742
+ - `put` stores a blob and returns the `storageRef` that the registry records.
743
+ - `get` returns a blob by its `storageRef`, as `{ kind: 'stream', stream }`,
744
+ `{ kind: 'bytes', bytes }`, or `{ kind: 'redirect', url }`.
745
+ - `delete` removes a blob by its `storageRef`.
746
+ - `createUpload` is optional. It enables direct uploads.
747
+
748
+ **Upload paths.** `publish` uploads only the blobs that the registry does not
749
+ have. The active provider decides which of two paths each blob takes. No
750
+ setting changes the path.
751
+
752
+ | Path | When | Hash check | Size limit |
753
+ | --- | --- | --- | --- |
754
+ | Through the registry | The `database` provider, or a custom provider without `createUpload` | The registry | The request limit of the host, about 4.5 MB on Vercel. The `database` provider also applies `maxInlineBytes`. |
755
+ | Direct to storage | A custom provider with `createUpload` | Storage, through the signed URL | The limit of the storage |
411
756
 
412
- ### 3. Installation
757
+ For a direct upload, the registry gives the CLI a signed URL, and the CLI sends
758
+ the blob to storage. The registry records the blob only after `get` finds the
759
+ stored object.
413
760
 
414
- The installer is opened at http://localhost:4000/install, where the first
415
- administrator is created. The deployment is verified by the installer, but
416
- migrations are never run by it.
761
+ - The machine that runs `publish` must be able to connect to the storage URL.
762
+ - If a direct upload fails, `publish` stops. It does not try again through the
763
+ registry.
764
+ - The signed URL must make storage reject bytes that do not match `hash`.
765
+ - The registry does not delete an uploaded object that it never records.
417
766
 
418
- An uninstalled registry can be claimed by the first visitor. A new deployment
419
- should not be left unattended before installation is complete.
767
+ **Download paths.** The result of `get` controls how a reader receives a blob.
768
+ With `{ kind: 'redirect' }`, the registry sends the reader to storage. With
769
+ `bytes` or `stream`, the registry sends the blob itself.
420
770
 
421
- ### 4. Workspace authorization
771
+ **Changing the provider.** The registry records which provider stores each blob.
772
+ Blobs in PostgreSQL stay readable after you select a custom provider. A publish
773
+ does not move blobs. If you change the custom provider or its storage, copy the
774
+ stored objects first. The new provider must find each object by the
775
+ `storageRef` that the previous provider returned. After a change back to
776
+ `database`, the registry cannot read the blobs of the custom provider.
422
777
 
423
- A workspace reaches a registry with a registry URL and an access token. There
424
- are two ways to supply them. Both use the same kind of token, and both are
425
- listed and revoked under Account → Access tokens in the registry.
778
+ > **Recipes:** [S3-compatible storage](RECIPES.md#s3-compatible-storage) ·
779
+ > [Vercel Blob](RECIPES.md#vercel-blob)
426
780
 
427
- #### Device login
781
+ ### Email
428
782
 
429
- CLI authorization is started from the source workspace with:
783
+ The registry sends invitation and password-reset emails when `email.from` is
784
+ set. Without it, an administrator sees each invitation link one time and sends
785
+ it manually, and password reset is not available.
786
+
787
+ ```ts
788
+ runtime: {
789
+ registry: {
790
+ email: {
791
+ from: 'Handoff <no-reply@example.com>',
792
+ provider: 'smtp',
793
+ options: { host: fromEnv('SMTP_HOST') },
794
+ },
795
+ },
796
+ },
797
+ ```
798
+
799
+ `provider` selects the delivery service. It is required when you set `email`.
800
+
801
+ - `smtp` sends through an SMTP server, for example Amazon SES, SendGrid, or
802
+ Microsoft 365.
803
+ - `custom` names a [custom server module](#custom-server-modules) that
804
+ default-exports `defineEmailProvider()`. Use it for an HTTP API, for example
805
+ Resend or Postmark.
806
+
807
+ `apiKey` and `password` must use `fromEnv()`. A profile can set a different
808
+ sender or provider. When a profile changes the provider, it replaces
809
+ `options`.
810
+
811
+ > **Recipes:** [SMTP](RECIPES.md#smtp) · [Resend](RECIPES.md#resend) ·
812
+ > [Custom email provider](RECIPES.md#custom-email-provider)
813
+
814
+ ### Custom server modules
815
+
816
+ Asset storage, email, and AI connections can name a custom server module. Each
817
+ module must obey these rules:
818
+
819
+ - The path is relative to the working directory.
820
+ - The file is `.js` or `.mjs`.
821
+ - The default export is the object, or a factory that returns it.
822
+ - The module imports its define helper from `handoff-app/providers`. The registry
823
+ build stops when a module imports `handoff-app`.
824
+ - The project installs the packages that the module imports. The registry
825
+ build copies them into the bundle.
826
+
827
+ The factory gets `options` with each `fromEnv()` value resolved at request
828
+ time.
829
+
830
+ ## Connecting a workspace to a registry
831
+
832
+ A connected workspace publishes content to a registry and checks content out
833
+ from it. It needs a registry URL and an access token. You can supply them with
834
+ a [device login](#device-login) or with [environment variables](#access-token).
835
+ Both use the same kind of token. You can see and revoke tokens under
836
+ Account → Access tokens in the registry.
837
+
838
+ ### Device login
839
+
840
+ Start the authorization from the workspace:
430
841
 
431
842
  ```bash
432
843
  npm run login -- --url http://localhost:4000
433
844
  ```
434
845
 
435
- Registry sign-in, entry of the displayed device code, and CLI approval are
436
- completed in the browser. The issued credential is saved in
437
- `.handoff/cli-auth.json` for that exact registry URL.
846
+ In the browser, sign in to the registry, enter the device code, and approve the
847
+ CLI. The CLI saves the credential in `.handoff/cli-auth.json` for that exact
848
+ registry URL.
438
849
 
439
- One credential is saved per profile, so a workspace can stay signed in to
440
- several registries at the same time:
850
+ The CLI saves one credential for each profile. Thus a workspace can stay signed
851
+ in to several registries:
441
852
 
442
853
  ```bash
443
854
  npm run login -- --profile staging --url https://staging.example.com
@@ -446,44 +857,55 @@ npm run publish -- all --profile staging
446
857
 
447
858
  A login profile name is free. It needs no `handoff.config.<name>.*` file, and
448
859
  `login` is the only command that accepts a new name. A profile without its own
449
- login falls back to the default login, so one `npm run login` is still enough
450
- for a workspace with one registry.
860
+ login uses the default login. Thus one `npm run login` is enough for a
861
+ workspace with one registry.
451
862
 
452
- `npm run logout` revokes and removes the login of the selected profile. The
453
- `--all` option removes every saved login.
863
+ `npm run logout` revokes and removes the login of the selected profile.
864
+ `--all` removes every saved login.
454
865
 
455
- #### Environment variables
866
+ ### Access token
456
867
 
457
- A token is created in the registry under Account → Access tokens. Read and
458
- write access is required to publish; read access is enough to check out. The
459
- token and the registry URL are then set in `.env`:
868
+ Create a token in the registry under Account → Access tokens. To publish, the
869
+ token needs read and write access. To check out, read access is enough. Set the
870
+ token and the registry URL in `.env`:
460
871
 
461
872
  ```dotenv
462
873
  HANDOFF_REGISTRY_URL=http://localhost:4000
463
874
  HANDOFF_REGISTRY_ACCESS_TOKEN=hnd_...
464
875
  ```
465
876
 
466
- No configuration file entry is needed, because these two variables are the
467
- defaults for `runtime.registryConnection`. A selected profile reads
468
- `.env.<profile>` on top of `.env`, so one workspace can address a different
469
- registry per profile. In CI, the job environment supplies the same two
470
- variables instead of a file.
877
+ These two variables are the defaults of `runtime.registryConnection`, so no
878
+ config entry is necessary. A selected profile reads `.env.<profile>` on top of
879
+ `.env`. Thus each profile can address a different registry. In CI, the job
880
+ environment supplies the same two variables.
471
881
 
472
- Environment values win over a saved device login, so a CI job stays
473
- deterministic on a machine where a developer is signed in. A token is only used
474
- for the registry URL it was issued for. If the environment names a different
475
- registry than the login of the selected profile, publish and checkout report
476
- which login was skipped and why.
882
+ Environment values have priority over a saved device login. Thus a CI job gives
883
+ the same result on a computer where a developer is signed in. A token works
884
+ only for the registry URL that issued it. If the environment names a different
885
+ registry than the login of the selected profile, `publish` and `checkout`
886
+ report which login they skipped and why.
477
887
 
478
- ### 5. Content publishing
888
+ A config block is necessary only to keep the URL in the repository, or to read
889
+ the values from variables with different names:
479
890
 
480
- Every kind in the workspace is published in dependency order with:
891
+ ```ts
892
+ runtime: {
893
+ registryConnection: {
894
+ url: 'https://registry.example.com',
895
+ accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN'),
896
+ },
897
+ },
898
+ ```
899
+
900
+ ### Publishing
901
+
902
+ Publish every kind of content in dependency order:
481
903
 
482
904
  ```bash
483
905
  npm run publish -- all
484
906
  ```
485
907
 
486
- A single kind is published on its own:
908
+ Publish one kind:
487
909
 
488
910
  ```bash
489
911
  npm run publish -- catalog
@@ -492,201 +914,186 @@ npm run publish -- tokens
492
914
  npm run publish -- assets
493
915
  ```
494
916
 
495
- `catalog` covers every catalog item. An item is stored as a component or as a
496
- pattern, and its declaration decides which. The command names items and never a
497
- kind.
917
+ `catalog` covers every catalog item. The declaration of each item decides if
918
+ the registry stores it as a component or as a pattern. Thus the command names
919
+ items, not component or pattern kinds.
498
920
 
499
- Publishing tokens or assets runs the Figma data pipeline before upload, so the
500
- documented Figma credentials must be available.
921
+ `tokens` and `assets` run the Figma pipeline before the upload. Thus the
922
+ [Figma credentials](#figma-foundations) must be available.
501
923
 
502
- One or more IDs can be appended to narrow a publish to those entities:
924
+ Add one or more ids to publish only those entities:
503
925
 
504
926
  ```bash
505
927
  npm run publish -- catalog item-id another-id
506
928
  ```
507
929
 
508
- `--dry-run` reports what would be uploaded and contacts no registry at all, so
509
- it needs neither a registry URL nor a token. It still runs the build, which
510
- refreshes generated output on disk; `--no-build` skips the build and publishes
511
- the existing output, and the two combine to leave the workspace untouched:
930
+ `--dry-run` lists the content that a real publish uploads. It does not connect
931
+ to a registry. Thus it needs no registry URL and no token. It still runs the build,
932
+ which updates the generated output on disk. `--no-build` skips the build and
933
+ publishes the existing output. Together, the two options do not change the
934
+ workspace:
512
935
 
513
936
  ```bash
514
937
  npm run publish -- all --dry-run
515
938
  npm run publish -- catalog --no-build
516
939
  ```
517
940
 
518
- `checkout` takes the same `all`, multi-ID, and `--dry-run` forms. A dry-run
519
- checkout reads from the registry, lists the files it would create or overwrite,
520
- and writes nothing.
941
+ After a publish, reload the registry to see the published items and
942
+ foundations.
521
943
 
522
- After the registry is reloaded, the published catalog items and foundations are
523
- visible. Published database records are read by registry pages; the local
524
- workspace is never read directly.
944
+ ### Checkout
525
945
 
526
- For CI, supply `HANDOFF_REGISTRY_URL` and `HANDOFF_REGISTRY_ACCESS_TOKEN`
527
- through the job environment, as described under
528
- [Workspace authorization](#4-workspace-authorization). A connection block in
529
- `handoff.config.ts` is needed only to pin the URL in the repository, or to read
530
- the values from differently named variables (`fromEnv` is imported from
531
- `handoff-app`):
946
+ `checkout` writes registry content into the workspace. It takes the same
947
+ `all`, kind, id, and `--dry-run` forms as `publish`:
532
948
 
533
- ```ts
534
- runtime: {
535
- registryConnection: {
536
- url: 'https://registry.example.com',
537
- accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN'),
538
- },
539
- },
949
+ ```bash
950
+ npm run checkout -- all
951
+ npm run checkout -- catalog item-id
952
+ npm run checkout -- all --dry-run
540
953
  ```
541
954
 
955
+ A dry-run checkout reads from the registry and lists the files that a real
956
+ checkout creates or overwrites. It writes nothing. Checkout writes declarations in the
957
+ format that `runtime.workspace.declarationFormat` sets.
958
+
959
+ > **Recipes:** [One workspace, several registries](RECIPES.md#one-workspace-several-registries) ·
960
+ > [Publish from GitHub Actions](RECIPES.md#publish-from-github-actions)
961
+
542
962
  ## MCP
543
963
 
544
- The documentation app serves a Model Context Protocol endpoint at `/api/mcp/`, so
545
- coding agents can look up the pages, components and tokens that already exist instead
546
- of inventing markup and values. It is stateless Streamable HTTP over `POST`;
547
- there is nothing to start and no extra port. Keep the trailing slash: the app
548
- sets `trailingSlash: true`, so `/api/mcp` answers with a 308 redirect.
964
+ The documentation app serves a Model Context Protocol endpoint at `/api/mcp/`.
965
+ Coding agents use it to find the pages, components, and tokens that exist. Thus
966
+ they do not invent markup and values. The endpoint uses stateless Streamable
967
+ HTTP over `POST`. There is nothing to start and no extra port.
549
968
 
550
969
  | Target | Endpoint | Credential |
551
970
  | --- | --- | --- |
552
- | Workspace (`dev` / `start`) | `http://localhost:3000/api/mcp/` | none, as with `/api/docs/*` |
553
- | Registry (standalone or Vercel) | `<registry url>/api/mcp/` | an access token, read-only is enough |
554
- | Static export | not available, a static site has no API routes | n/a |
971
+ | Workspace (`dev` / `start`) | `http://localhost:3000/api/mcp/` | None, as with `/api/docs/*` |
972
+ | Registry (standalone or Vercel) | `<registry url>/api/mcp/` | An access token. Read access is enough. |
973
+ | Static site | Not available | Not applicable |
974
+
975
+ Keep the trailing slash. The app sets `trailingSlash: true`, so `/api/mcp`
976
+ answers with a 308 redirect. Registry tokens come from `handoff-app login` or
977
+ from Account → Access tokens in the registry.
978
+
979
+ The documentation app shows its endpoint and a ready-made client config for
980
+ Claude Code, Cursor, and VS Code behind the plug icon in the header.
981
+
982
+ MCP is on by default. To turn it off, set `runtime.mcp: false`. The build bakes
983
+ this value. A build with MCP off answers 404 on the route, leaves the MCP SDK
984
+ out of the bundle, and shows no connect control.
555
985
 
556
- Registry tokens come from `handoff-app login` or Account settings in the
557
- registry itself, and are the same credentials the rest of the registry API takes.
986
+ > **Recipes:** [Connect a client by hand](RECIPES.md#connect-a-client-by-hand) ·
987
+ > [Turn off MCP](RECIPES.md#turn-off-mcp)
558
988
 
559
- The endpoint is served unless a project turns it off:
989
+ ## AI assistant
990
+
991
+ The documentation app can answer questions about the design system. The
992
+ assistant reads through the same MCP tools. Thus each answer comes from the
993
+ catalog and links to the pages and components that it read. Open the assistant
994
+ from the search control in the header, or with `⌘K`.
995
+
996
+ The `ai` block turns on the assistant, and the deployment selects the
997
+ provider:
560
998
 
561
999
  ```ts
562
1000
  runtime: {
563
- mcp: false,
1001
+ ai: {
1002
+ connections: [
1003
+ {
1004
+ id: 'gateway',
1005
+ label: 'Acme LiteLLM',
1006
+ baseUrl: 'https://llm.acme.internal/v1',
1007
+ apiKey: fromEnv('LITELLM_API_KEY'),
1008
+ models: ['gpt-4o', 'claude-sonnet-4-5'],
1009
+ },
1010
+ ],
1011
+ defaultModel: 'gateway/claude-sonnet-4-5',
1012
+ },
564
1013
  },
565
1014
  ```
566
1015
 
567
- Like `runtime.mode`, this is config-only and baked at build time, so changing it
568
- in a deployed registry means a rebuild. A disabled build answers 404 on the route,
569
- drops the MCP SDK from the packaged bundle, and shows no connect affordance at all
570
- rather than one that fails.
1016
+ `ai: { enabled: false }` turns off the assistant. The build bakes this flag.
1017
+ A build without the assistant serves no chat route and shows no control.
571
1018
 
572
- The documentation app carries its own endpoint and a ready-made client config
573
- for Claude Code, Cursor and VS Code behind the plug icon in the header. By hand:
1019
+ Each connection uses one of these credentials:
574
1020
 
575
- ```json
576
- {
577
- "mcpServers": {
578
- "handoff": {
579
- "type": "http",
580
- "url": "https://registry.example.com/api/mcp/",
581
- "headers": { "Authorization": "Bearer hnd_..." }
582
- }
583
- }
584
- }
585
- ```
1021
+ - **Service key:** `apiKey` names a deployment key. The deployment pays for
1022
+ every reader.
1023
+ - **No credential:** the endpoint needs no key, for example a local Ollama.
1024
+ - **Reader key:** `credential: 'user'` asks each reader for their own key.
586
1025
 
587
- ## Configuration
1026
+ **The declared connections are the full surface.** A reader can add a key only
1027
+ for a connection that the config declares. Thus the server never calls a URL
1028
+ that a reader selected. Without a `credential: 'user'` connection, readers have
1029
+ no AI setting.
588
1030
 
589
- Configuration is read from `handoff.config.ts`, `.js`, `.cjs`, or `.json`, in
590
- that order. `defineConfig` provides typed authoring. Values merge onto the
591
- defaults: plain objects merge recursively, and arrays, scalars, `null`, and
592
- functions replace.
1031
+ Each connection uses the OpenAI-compatible `/chat/completions` API. This does
1032
+ not limit the models. Ollama serves an OpenAI-compatible API at `/v1`. LiteLLM,
1033
+ OpenRouter, vLLM, LM Studio, and Azure OpenAI each give access to Anthropic,
1034
+ Google, and xAI models. For a provider without this API, name a
1035
+ [custom server module](#custom-server-modules) that default-exports
1036
+ `defineAiProvider()`, instead of `baseUrl`.
593
1037
 
594
- Useful environment variables:
1038
+ The gateway controls cost. LiteLLM and OpenRouter both apply budgets and rate
1039
+ limits for each key. The assistant limits only the number of steps for one
1040
+ question.
595
1041
 
596
- | Variable | Purpose |
597
- | --- | --- |
598
- | `HANDOFF_PROFILE` | Config profile merged onto the base config |
599
- | `HANDOFF_FIGMA_PROJECT_ID` | Figma file ID used by `fetch` |
600
- | `HANDOFF_DEV_ACCESS_TOKEN` | Figma personal access token used by `fetch` |
601
- | `HANDOFF_REGISTRY_URL` | Connected workspace registry URL |
602
- | `HANDOFF_REGISTRY_ACCESS_TOKEN` | Registry access token used by a connected workspace |
603
- | `HANDOFF_SYNC_SECRET` | Optional deployment-wide registry credential |
604
- | `DATABASE_URL` | Registry PostgreSQL connection string |
605
- | `AUTH_SECRET` | Registry session-signing secret, at least 32 characters |
606
- | `AUTH_URL` | Canonical public registry URL |
607
- | `PORT` | Standalone registry server port |
608
- | `HOSTNAME` | Standalone registry bind hostname |
609
- | `BLOB_READ_WRITE_TOKEN` | Credential for the Vercel Blob asset adapter |
610
- | `HANDOFF_OUTPUT_DIR` | Override the fetched output directory |
611
- | `HANDOFF_SITES_DIR` | Override the build output directory |
612
- | `HANDOFF_APP_PORT` | Workspace documentation server port |
613
- | `HANDOFF_WEBSOCKET_PORT` | Workspace live-reload server port |
614
-
615
- Registry assets are stored in PostgreSQL by default with a 4 MB per-blob limit.
616
- Vercel Blob or a custom adapter can be selected through
617
- `runtime.registry.assetStorage`. The `pg` or `neon` connection driver can be
618
- selected through `runtime.registry.database.driver`; PostgreSQL is used by
619
- both.
620
-
621
- ## Profiles
622
-
623
- A profile is a sidecar config file that merges onto the base config. It holds
624
- only what changes between environments, so a project keeps one shared config
625
- instead of a second complete copy of it.
626
-
627
- - The base config is `handoff.config.ts`, `.js`, `.cjs`, or `.json`. It is the
628
- only config file loaded when no profile is selected.
629
- - A profile file is `handoff.config.<profile>.*`, with the same four
630
- extensions.
631
- - Select a profile with `--profile <name>` or with `HANDOFF_PROFILE`.
632
- `--profile` takes precedence over `HANDOFF_PROFILE`.
633
- - Profile names are not predefined. A name can contain lowercase letters,
634
- numbers, and hyphens.
635
- - A selected profile must exist. If no `handoff.config.<name>.*` file is found,
636
- the command stops with an error.
637
- - `login`, `logout`, `publish`, and `checkout` also accept a profile that has
638
- only a saved registry login, because there the profile selects the registry.
639
- Every other command still needs the config file.
640
- - Layers resolve as defaults, base config, profile, then programmatic config,
641
- with the merge rules described under [Configuration](#configuration).
642
- - A selected profile also reads `.env.<profile>` on top of `.env`, when that
643
- file exists. Both are read from the directory the command runs in, and a
644
- variable already set in the environment beats both files.
1042
+ ### Reader keys
645
1043
 
646
- ```bash
647
- npx handoff-app build --target registry --profile registry
648
- ```
1044
+ A reader adds a key under Account → AI providers. The registry encrypts keys
1045
+ with `HANDOFF_AI_KEY_SECRET`. It does not hash them, because the server must
1046
+ send them to the provider. Set this variable to a random value of at least 32
1047
+ characters. A key is write-only through the API. After a save, a read reports
1048
+ only that a key exists.
1049
+
1050
+ Reader keys work only in registry mode. A workspace has one user, whose config
1051
+ file and `.env` hold their keys. Thus a workspace supplies a key through
1052
+ `apiKey`.
649
1053
 
650
- `build --target registry` always packages a registry-mode application, so a
651
- profile does not need to set `runtime.mode`. This makes a profile a good place
652
- for the registry build settings: the database driver, the database
653
- environment-variable name, the asset storage, and whether MCP is enabled. A
654
- hosting provider or a CI job selects the profile through `HANDOFF_PROFILE`.
1054
+ ### Changing connections without a rebuild
655
1055
 
656
- `defineConfig` types a profile as well as a base config. The generated
657
- `.gitignore` lists `handoff.config.local.*`, which makes `local` the usual name
658
- for a profile that stays on one machine.
1056
+ The connection list is deployment data, not build shape. Thus a registry can
1057
+ change it without a rebuild. `HANDOFF_AI_CONNECTIONS` holds a JSON array. The
1058
+ registry reads it at request time and merges it over the built-in list by `id`.
1059
+ Each entry names its key variable with `apiKeyEnv`, not the key value.
659
1060
 
660
- Arrays replace, so a profile that declares `catalog.include` also decides what
661
- `make` and `checkout` register.
1061
+ A connection in this list can use a `module` only if a connection in the config
1062
+ names the same module. The build copies only those modules into the registry.
662
1063
 
663
- `.env.<profile>` is read once at startup rather than watched.
664
- `HANDOFF_WORKING_PATH` cannot be set from it, because the working path is
665
- resolved before any profile is known.
1064
+ > **Recipes:** [Service key through a gateway](RECIPES.md#service-key-through-a-gateway) ·
1065
+ > [Local Ollama](RECIPES.md#local-ollama) · [Reader keys](RECIPES.md#reader-keys) ·
1066
+ > [Custom AI provider](RECIPES.md#custom-ai-provider) ·
1067
+ > [Change connections without a rebuild](RECIPES.md#change-connections-without-a-rebuild)
666
1068
 
667
1069
  ## CLI reference
668
1070
 
669
1071
  | Command | Description |
670
1072
  | --- | --- |
671
- | `npx handoff-app init` | A workspace is scaffolded with the interactive wizard |
672
- | `npm run fetch` | Design tokens and assets are fetched from Figma |
673
- | `npm run start` | The workspace server and Handoff file watchers are started |
674
- | `npm run dev` | The workspace development server is started |
675
- | `npm run build -- [--target static\|registry]` | The static site or standalone registry bundle is built |
676
- | `npm run db:migrate` | Registry database migrations are applied |
677
- | `npm run validate` | Configured components are validated |
678
- | `npm run publish -- <kind\|all> [id...]` | Catalog items, pages, tokens, or assets are published |
679
- | `npm run checkout -- <kind\|all> [id...]` | Published content is pulled into a workspace |
680
- | `npm run login -- [--profile <name>] --url <url>` | The CLI is authorized through the registry device flow |
681
- | `npm run logout -- [--profile <name>] [--all]` | Saved CLI credentials are revoked and removed |
1073
+ | `npx handoff-app init` | Create a project with the interactive wizard |
1074
+ | `npm run fetch` | Fetch design tokens and assets from Figma |
1075
+ | `npm run start` | Start the workspace server and the Handoff file watchers |
1076
+ | `npm run dev` | Start the workspace server without the file watchers |
1077
+ | `npm run build -- [--target static\|registry] [--package vercel]` | Build the static site or the registry server |
1078
+ | `npm run db:migrate` | Apply the registry database migrations |
1079
+ | `npm run publish -- <kind\|all> [id...]` | Publish catalog items, pages, tokens, or assets |
1080
+ | `npm run checkout -- <kind\|all> [id...]` | Write published content into the workspace |
1081
+ | `npm run login -- [--profile <name>] --url <url>` | Authorize the CLI through the registry device flow |
1082
+ | `npm run logout -- [--profile <name>] [--all]` | Revoke and remove saved CLI credentials |
1083
+ | `npx handoff-app validate:components` | Validate the catalog items |
1084
+ | `npx handoff-app make:component <name>` | Create a Handlebars catalog item |
1085
+ | `npx handoff-app make:page <name> [parent]` | Create a documentation page |
1086
+ | `npx handoff-app scaffold` | Create catalog item stubs for fetched Figma components |
1087
+ | `npx handoff-app eject:config` | Write the default config to the working directory |
682
1088
 
683
1089
  Every command except `init` accepts `-c, --config`, `--profile`, `-d, --debug`,
684
- and `-f, --force`. `publish` and `checkout` additionally accept `--dry-run`, and
1090
+ and `-f, --force`. `publish` and `checkout` also accept `--dry-run`, and
685
1091
  `publish` accepts `--no-build`.
686
1092
 
687
- Arguments after `--` are forwarded to the local CLI. Exact options can be shown
688
- by adding `--help` after the separator. Advanced configuration and hooks are
689
- documented in [docs/api.md](docs/api.md).
1093
+ npm forwards the arguments after `--` to the CLI. To see the options of a
1094
+ command, add `--help` after `--`. [docs/cli.md](docs/cli.md) lists every
1095
+ command and option. [docs/api.md](docs/api.md) documents the JavaScript API and
1096
+ the build hooks.
690
1097
 
691
1098
  ## Maintainers
692
1099
 
@@ -706,4 +1113,4 @@ Code of Conduct.
706
1113
 
707
1114
  ## License
708
1115
 
709
- [MIT](License.md) ©Convertiv
1116
+ [MIT](LICENSE.md) ©Convertiv