handoff-app 2.0.0-8 → 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 (322) hide show
  1. package/{Changelog.md → CHANGELOG.md} +85 -1
  2. package/{License.md → LICENSE.md} +1 -1
  3. package/README.md +816 -393
  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/catalog/normalize.js +3 -0
  55. package/dist/catalog/previews.js +5 -4
  56. package/dist/catalog/types.d.ts +8 -0
  57. package/dist/cli/create.js +1 -2
  58. package/dist/commands/build/index.d.ts +1 -1
  59. package/dist/commands/build/index.js +1 -1
  60. package/dist/commands/utils.d.ts +3 -3
  61. package/dist/config/defaults.js +7 -4
  62. package/dist/config/from-env.d.ts +4 -0
  63. package/dist/config/from-env.js +38 -1
  64. package/dist/config/helpers.js +2 -2
  65. package/dist/config/index.d.ts +0 -1
  66. package/dist/config/index.js +1 -4
  67. package/dist/config/loader.js +28 -2
  68. package/dist/config/resolve-env.d.ts +2 -1
  69. package/dist/config/resolve-env.js +28 -12
  70. package/dist/config/runtime.js +9 -11
  71. package/dist/handlebars.d.ts +10 -2
  72. package/dist/index.d.ts +12 -7
  73. package/dist/index.js +18 -22
  74. package/dist/nav/index.d.ts +2 -2
  75. package/dist/nav/registry.d.ts +3 -3
  76. package/dist/nav/registry.js +1 -1
  77. package/dist/nav/workspace.d.ts +3 -3
  78. package/dist/nav/workspace.js +1 -1
  79. package/dist/pipeline/figma.d.ts +13 -2
  80. package/dist/pipeline/figma.js +86 -46
  81. package/dist/pipeline/styles.js +1 -1
  82. package/dist/pipeline/validation.d.ts +1 -1
  83. package/dist/pipeline/validation.js +3 -3
  84. package/dist/providers.d.ts +10 -0
  85. package/dist/providers.js +13 -0
  86. package/dist/registry/asset-storage/define.d.ts +5 -5
  87. package/dist/registry/asset-storage/define.js +4 -4
  88. package/dist/registry/asset-storage/resolve.d.ts +9 -19
  89. package/dist/registry/asset-storage/resolve.js +33 -21
  90. package/dist/registry/asset-storage/types.d.ts +37 -17
  91. package/dist/registry/asset-storage/types.js +5 -5
  92. package/dist/registry/auth/ai-keys.d.ts +32 -0
  93. package/dist/registry/auth/ai-keys.js +69 -0
  94. package/dist/registry/auth/crypto.d.ts +17 -0
  95. package/dist/registry/auth/crypto.js +61 -1
  96. package/dist/registry/auth/index.d.ts +2 -1
  97. package/dist/registry/auth/index.js +11 -2
  98. package/dist/registry/checkout/index.d.ts +2 -1
  99. package/dist/registry/checkout/index.js +57 -127
  100. package/dist/registry/client.js +53 -17
  101. package/dist/registry/db/driver.js +3 -3
  102. package/dist/registry/db/schema.d.ts +261 -3
  103. package/dist/registry/db/schema.js +29 -4
  104. package/dist/registry/email/define.d.ts +10 -0
  105. package/dist/registry/email/define.js +13 -0
  106. package/dist/registry/email/providers/smtp.d.ts +10 -0
  107. package/dist/registry/email/providers/smtp.js +73 -0
  108. package/dist/registry/email/resolve.d.ts +28 -0
  109. package/dist/registry/email/resolve.js +74 -0
  110. package/dist/registry/email/types.d.ts +24 -0
  111. package/dist/registry/email/types.js +6 -0
  112. package/dist/registry/path.d.ts +2 -0
  113. package/dist/registry/path.js +11 -1
  114. package/dist/registry/publish/package.js +8 -0
  115. package/dist/registry/transfer.d.ts +4 -0
  116. package/dist/store/registry.d.ts +3 -3
  117. package/dist/store/registry.js +4 -4
  118. package/dist/transformers/plugins/ssr-render.js +40 -15
  119. package/dist/transformers/preview/component/builder.d.ts +1 -9
  120. package/dist/transformers/preview/component/builder.js +17 -17
  121. package/dist/transformers/preview/component/css.d.ts +3 -1
  122. package/dist/transformers/preview/component/css.js +92 -51
  123. package/dist/transformers/preview/component/html.d.ts +3 -1
  124. package/dist/transformers/preview/component/html.js +6 -2
  125. package/dist/transformers/preview/component/javascript.d.ts +2 -1
  126. package/dist/transformers/preview/component/javascript.js +10 -4
  127. package/dist/transformers/preview/pattern/html.js +3 -1
  128. package/dist/transformers/preview/types.d.ts +13 -0
  129. package/dist/transformers/utils/build.d.ts +24 -0
  130. package/dist/transformers/utils/build.js +100 -1
  131. package/dist/transformers/utils/module.js +2 -1
  132. package/dist/types/config.d.ts +135 -18
  133. package/dist/utils/path.d.ts +1 -5
  134. package/dist/utils/path.js +6 -23
  135. package/docs/api.md +6 -4
  136. package/docs/cli.md +275 -54
  137. package/drizzle/0007_add_ai_keys.sql +11 -0
  138. package/drizzle/0008_add_source_dir.sql +2 -0
  139. package/drizzle/meta/0007_snapshot.json +2013 -0
  140. package/drizzle/meta/0008_snapshot.json +2025 -0
  141. package/drizzle/meta/_journal.json +14 -0
  142. package/package.json +19 -8
  143. package/src/ai/README.md +42 -0
  144. package/src/ai/connections.ts +189 -0
  145. package/src/ai/define.ts +15 -0
  146. package/src/ai/types.ts +30 -0
  147. package/src/app/components/AiAssistant/AiAssistantPanel.tsx +144 -0
  148. package/src/app/components/AiAssistant/AiAssistantProvider.tsx +215 -0
  149. package/src/app/components/AiAssistant/AiConversation.tsx +737 -0
  150. package/src/app/components/AiAssistant/AiToolResult.tsx +99 -0
  151. package/src/app/components/AiAssistant/dockState.ts +86 -0
  152. package/src/app/components/AiAssistant/toolLabel.ts +24 -0
  153. package/src/app/components/AiAssistant/useAiChat.ts +147 -0
  154. package/src/app/components/AiAssistant/useAiConnections.ts +86 -0
  155. package/src/app/components/AiAssistant/usePageContext.ts +63 -0
  156. package/src/app/components/Auth/AccountLayout.tsx +58 -19
  157. package/src/app/components/Auth/AuthShell.tsx +27 -33
  158. package/src/app/components/Auth/RegistryInstallGate.tsx +4 -8
  159. package/src/app/components/Auth/installStatus.ts +29 -0
  160. package/src/app/components/Component/PageSliceResolver.tsx +1 -0
  161. package/src/app/components/Component/Preview.tsx +383 -189
  162. package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +4 -0
  163. package/src/app/components/Foundations/ColorGrid.tsx +13 -14
  164. package/src/app/components/Layout/Header.tsx +21 -15
  165. package/src/app/components/Layout/Main.tsx +2 -2
  166. package/src/app/components/Markdown/MarkdownComponents.tsx +30 -32
  167. package/src/app/components/McpIntegration/McpConfigDialog.tsx +12 -51
  168. package/src/app/components/Navigation/MobileNav.tsx +2 -2
  169. package/src/app/components/Navigation/SideNav.tsx +1 -15
  170. package/src/app/components/context/NavProvider.tsx +7 -3
  171. package/src/app/components/ui/attachment.tsx +50 -0
  172. package/src/app/components/ui/bubble.tsx +57 -0
  173. package/src/app/components/ui/button.tsx +3 -1
  174. package/src/app/components/ui/confirm-dialog.tsx +87 -0
  175. package/src/app/components/ui/dialog.tsx +7 -2
  176. package/src/app/components/ui/marker.tsx +56 -0
  177. package/src/app/components/ui/sidebar.tsx +3 -1
  178. package/src/app/components/util/index.ts +3 -12
  179. package/src/app/css/index.css +64 -0
  180. package/src/app/lib/ai/auth.ts +33 -0
  181. package/src/app/lib/ai/context.ts +99 -0
  182. package/src/app/lib/ai/model.ts +79 -0
  183. package/src/app/lib/ai/prompt.ts +36 -0
  184. package/src/app/lib/ai/resolve.ts +97 -0
  185. package/src/app/lib/ai/tools.ts +120 -0
  186. package/src/app/lib/api/methods.ts +13 -0
  187. package/src/app/lib/asset-storage.ts +35 -36
  188. package/src/app/lib/auth/api.ts +4 -12
  189. package/src/app/lib/auth/email.ts +96 -23
  190. package/src/app/lib/docs-api/page-rendering.ts +1 -1
  191. package/src/app/lib/docs-api/registry.ts +2 -3
  192. package/src/app/lib/docs-api/runtime-config.ts +47 -21
  193. package/src/app/lib/mcp/components.ts +523 -0
  194. package/src/app/lib/mcp/page-url.ts +24 -0
  195. package/src/app/lib/mcp/pages.ts +84 -0
  196. package/src/app/lib/mcp/result.ts +33 -0
  197. package/src/app/lib/mcp/server.ts +39 -318
  198. package/src/app/lib/mcp/tokens.ts +334 -0
  199. package/src/app/lib/mcp/tool.ts +19 -0
  200. package/src/app/lib/registry-api/asset-transfer.ts +119 -11
  201. package/src/app/lib/registry-api/errors.ts +2 -0
  202. package/src/app/lib/registry-api/handler.ts +7 -1
  203. package/src/app/lib/registry-api/transfer.ts +28 -6
  204. package/src/app/lib/server-module.ts +34 -0
  205. package/src/app/lib/utils.ts +20 -0
  206. package/src/app/next.config.mjs +26 -24
  207. package/src/app/pages/[...slug]/index.tsx +2 -1
  208. package/src/app/pages/_app.tsx +6 -2
  209. package/src/app/pages/_document.tsx +6 -0
  210. package/src/app/pages/account/ai.tsx +183 -0
  211. package/src/app/pages/account/index.tsx +68 -68
  212. package/src/app/pages/account/tokens.tsx +37 -7
  213. package/src/app/pages/account/users.tsx +75 -15
  214. package/src/app/pages/api/account/ai/keys/[connectionId].ts +60 -0
  215. package/src/app/pages/api/account/ai/keys/index.ts +20 -0
  216. package/src/app/pages/api/admin/users/[id]/[action].ts +80 -11
  217. package/src/app/pages/api/admin/users/index.ts +2 -2
  218. package/src/app/pages/api/ai/chat.ts +115 -0
  219. package/src/app/pages/api/ai/connections.ts +23 -0
  220. package/src/app/pages/api/auth/request-reset.ts +14 -16
  221. package/src/app/pages/api/docs/nav.json.ts +1 -1
  222. package/src/app/pages/api/install.ts +3 -3
  223. package/src/app/pages/api/mcp.ts +6 -0
  224. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/complete.ts +11 -0
  225. package/src/app/pages/api/registry/transfer/assets/blobs/[hash]/upload.ts +12 -0
  226. package/src/app/pages/api/registry/transfer/assets/blobs/[hash].ts +1 -1
  227. package/src/app/pages/cli/device.tsx +10 -5
  228. package/src/app/pages/foundations/colors.tsx +9 -8
  229. package/src/app/pages/foundations/index.tsx +3 -3
  230. package/src/app/pages/index.tsx +2 -2
  231. package/src/app/pages/install.tsx +7 -2
  232. package/src/app/pages/login.tsx +32 -27
  233. package/src/app/pages/reset-password.tsx +21 -20
  234. package/src/app/pages/system/component/[component]/index.tsx +2 -5
  235. package/src/app/pages/system/index.tsx +11 -11
  236. package/src/app/pages/system/pattern/[pattern]/index.tsx +2 -2
  237. package/src/app/pages/system/tokens/components/[component]/index.tsx +0 -4
  238. package/src/app-builder/README.md +2 -1
  239. package/src/app-builder/build.ts +218 -73
  240. package/src/app-builder/client-config.ts +3 -2
  241. package/src/app-builder/paths.ts +3 -22
  242. package/src/app-builder/static-export.ts +1 -1
  243. package/src/app-builder/vercel-output.ts +5 -4
  244. package/src/cache/README.md +4 -1
  245. package/src/cache/build-cache.ts +128 -43
  246. package/src/cache/build-inputs.ts +28 -0
  247. package/src/cache/index.ts +4 -0
  248. package/src/catalog/README.md +3 -0
  249. package/src/catalog/normalize.ts +2 -0
  250. package/src/catalog/previews.ts +6 -3
  251. package/src/catalog/types.ts +8 -0
  252. package/src/cli/create.ts +1 -2
  253. package/src/commands/build/index.ts +1 -1
  254. package/src/config/README.md +10 -9
  255. package/src/config/defaults.ts +7 -4
  256. package/src/config/from-env.ts +33 -0
  257. package/src/config/helpers.ts +1 -3
  258. package/src/config/index.ts +0 -3
  259. package/src/config/loader.ts +27 -3
  260. package/src/config/resolve-env.ts +28 -15
  261. package/src/config/runtime.ts +10 -5
  262. package/src/handlebars.ts +8 -2
  263. package/src/index.ts +33 -29
  264. package/src/nav/index.ts +2 -2
  265. package/src/nav/registry.ts +3 -3
  266. package/src/nav/workspace.ts +3 -3
  267. package/src/pipeline/README.md +1 -1
  268. package/src/pipeline/figma.ts +98 -49
  269. package/src/pipeline/styles.ts +1 -1
  270. package/src/pipeline/validation.ts +3 -3
  271. package/src/providers.ts +18 -0
  272. package/src/registry/asset-storage/define.ts +6 -6
  273. package/src/registry/asset-storage/resolve.ts +38 -37
  274. package/src/registry/asset-storage/types.ts +39 -17
  275. package/src/registry/auth/ai-keys.ts +62 -0
  276. package/src/registry/auth/crypto.ts +63 -1
  277. package/src/registry/auth/index.ts +11 -1
  278. package/src/registry/checkout/index.ts +60 -138
  279. package/src/registry/client.ts +60 -20
  280. package/src/registry/db/driver.ts +2 -2
  281. package/src/registry/db/schema.ts +49 -22
  282. package/src/registry/email/define.ts +15 -0
  283. package/src/registry/email/providers/smtp.ts +39 -0
  284. package/src/registry/email/resolve.ts +94 -0
  285. package/src/registry/email/types.ts +28 -0
  286. package/src/registry/path.ts +11 -0
  287. package/src/registry/publish/package.ts +9 -0
  288. package/src/registry/transfer.ts +4 -0
  289. package/src/store/registry.ts +7 -7
  290. package/src/transformers/plugins/ssr-render.ts +53 -16
  291. package/src/transformers/preview/component/builder.ts +19 -25
  292. package/src/transformers/preview/component/css.ts +101 -53
  293. package/src/transformers/preview/component/html.ts +6 -2
  294. package/src/transformers/preview/component/javascript.ts +14 -4
  295. package/src/transformers/preview/pattern/html.ts +3 -1
  296. package/src/transformers/preview/types.ts +14 -0
  297. package/src/transformers/utils/build.ts +93 -0
  298. package/src/transformers/utils/module.ts +4 -2
  299. package/src/types/config.ts +148 -20
  300. package/src/utils/README.md +1 -1
  301. package/src/utils/path.ts +6 -29
  302. package/templates/default/handoff.config.js.tpl +10 -2
  303. package/templates/default/handoff.config.ts.tpl +10 -2
  304. package/templates/default/handoff.config.with-examples.js.tpl +10 -2
  305. package/templates/default/handoff.config.with-examples.ts.tpl +10 -2
  306. package/templates/default/package.json.tpl +2 -2
  307. package/.editorconfig +0 -19
  308. package/.eslintrc.json +0 -4
  309. package/.prettierignore +0 -0
  310. package/.prettierrc.json +0 -8
  311. package/.vscode/settings.json +0 -17
  312. package/dist/config/validator.d.ts +0 -9
  313. package/dist/config/validator.js +0 -24
  314. package/dist/registry/asset-storage/adapters/vercel-blob.d.ts +0 -17
  315. package/dist/registry/asset-storage/adapters/vercel-blob.js +0 -87
  316. package/src/app/components/old/Header.tsx +0 -89
  317. package/src/app/lib/mcp/shape.ts +0 -596
  318. package/src/app/pages/assets/fonts.tsx +0 -129
  319. package/src/app/pages/assets/index.tsx +0 -91
  320. package/src/app/pages/assets/logos.tsx +0 -75
  321. package/src/config/validator.ts +0 -22
  322. package/src/registry/asset-storage/adapters/vercel-blob.ts +0 -51
package/README.md CHANGED
@@ -3,88 +3,446 @@
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
67
129
  ```
68
130
 
69
- ## Catalog items
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.
70
183
 
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.
184
+ To add Handoff in the recommended subdirectory layout:
74
185
 
75
- ### React
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
+ });
269
+ ```
76
270
 
77
- Handoff finds the implementation file from its import, so you do not need to repeat the path.
78
- Previews are named exports. `Preview<typeof item>` gives them the implementation's argument type.
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
421
+
422
+ Every documented UI entry is a catalog item. An item declares one of these:
423
+
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.
427
+
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.
79
436
 
80
437
  ```tsx
81
438
  // components/example/Component.tsx
82
439
  export type ComponentProps = {
83
440
  label: string;
441
+ onSelect?: () => void;
84
442
  };
85
443
 
86
- export default function Component({ label }: ComponentProps) {
87
- return <div>{label}</div>;
444
+ export default function Component({ label, onSelect }: ComponentProps) {
445
+ return <div onClick={onSelect}>{label}</div>;
88
446
  }
89
447
  ```
90
448
 
@@ -110,74 +468,23 @@ export const Default = {
110
468
  } satisfies ComponentPreview;
111
469
  ```
112
470
 
113
- The export name is the preview name. Use `name` to set a different display title.
114
-
115
- ### Handlebars
116
-
117
- The implementation is the path to the template. Arguments are `Record<string, unknown>` by default;
118
- an explicit argument type is supplied through `defineCatalogItem<BadgeArgs>`.
119
-
120
- ```ts
121
- // components/badge/badge.handoff.ts
122
- import { defineCatalogItem, type Preview } from 'handoff-app/handlebars';
123
-
124
- const item = defineCatalogItem({
125
- id: 'badge',
126
- name: 'Badge',
127
- implementation: './Badge.hbs',
128
- });
129
-
130
- export default item;
131
-
132
- type BadgePreview = Preview<typeof item>;
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.
133
475
 
134
- export const Primary = {
135
- args: { variant: 'primary', children: 'New' },
136
- } satisfies BadgePreview;
137
- ```
476
+ **Handlebars.** The implementation is the path to the template:
477
+ `implementation: './Badge.hbs'`, from `handoff-app/handlebars`.
138
478
 
139
- ### CSF
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.
140
484
 
141
- 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`.
142
- The CSF file stays unchanged and defines its previews through named exports. Each story's `args` merge with and override `meta.args`.
143
- Handoff preserves story names, `argTypes`, and `render` functions.
144
- It finds the React component through `meta.component`, then documents and publishes it with the story file.
145
-
146
- ```ts
147
- // components/card/card.handoff.ts
148
- import { defineCatalogItem, fromCSF } from 'handoff-app/react';
149
-
150
- export default defineCatalogItem({
151
- id: 'card',
152
- name: 'Card',
153
- implementation: fromCSF('./Card.stories.tsx'),
154
- });
155
- ```
156
-
157
- `Meta` and `StoryObj` are exported from `handoff-app/react`, so a story file type-checks without a
158
- Storybook dependency. A project that already has Storybook keeps using its own types.
159
-
160
- ```tsx
161
- // components/card/Card.stories.tsx
162
- import { type Meta, type StoryObj } from 'handoff-app/react';
163
- import Card from './Card';
164
-
165
- const meta = { component: Card } satisfies Meta<typeof Card>;
166
- export default meta;
167
-
168
- export const Primary: StoryObj<typeof meta> = {
169
- args: { title: 'Example' },
170
- };
171
- ```
172
-
173
- A CSF preview is rendered on the server and is not hydrated. A direct React implementation is
174
- hydrated in the browser.
175
-
176
- ### Compositions
177
-
178
- A composition references other items by stable id. A named preview can be selected and its arguments
179
- can be overridden by each reference. A composition needs no renderer, so it is declared with
180
- `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`:
181
488
 
182
489
  ```ts
183
490
  // patterns/example/example.handoff.ts
@@ -195,233 +502,353 @@ export default defineCatalogItem({
195
502
  });
196
503
  ```
197
504
 
198
- An item declares `implementation` or `composition`, never both. Terms such as atom, element, and
199
- block stay optional classification metadata on `type` and `categories`.
200
-
201
- The `id` addresses the item everywhere: its artifacts, its documentation URL, and its publish and
202
- checkout commands. Components and patterns share one namespace, so each id must be unique across
203
- 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.
204
510
 
205
- If two declarations claim one id, Handoff keeps the first and skips the second. The warning names
206
- 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`.
207
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)
208
517
 
209
518
  ### Registration
210
519
 
211
- Catalog directories are registered in one place. A path is either an item directory or a collection
212
- 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:
213
522
 
214
- ```ts
215
- // handoff.config.ts
216
- import { defineConfig } from 'handoff-app';
523
+ - An item directory.
524
+ - A collection directory. Each subdirectory is an item.
217
525
 
218
- export default defineConfig({
219
- catalog: {
220
- include: ['components', 'patterns'],
221
- },
222
- runtime: {
223
- workspace: {
224
- declarationFormat: 'ts',
225
- },
226
- },
227
- });
526
+ ```ts
527
+ catalog: {
528
+ include: ['components', 'patterns'],
529
+ },
228
530
  ```
229
531
 
230
- ### 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.
231
534
 
232
- See the [version 1.x.x to version 2.x.x migration guide](UPGRADE.md#version-1xx-to-version-2xx)
233
- for breaking changes, migration steps, and upgrade verification.
535
+ ### Pages
234
536
 
235
- Custom documentation pages are Markdown files under `pages/`. Their relative
236
- 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.
237
542
 
238
- ## Figma foundations
543
+ ### Figma foundations
239
544
 
240
- Figma integration is optional. When Figma credentials are entered during
241
- `init`, they are written to `.env` by the wizard and the fetch command can be
242
- 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.
243
547
 
244
- When that step is skipped or the Figma source must be changed, these values can
245
- be added or updated in `.env`:
548
+ To add or change the Figma source, set these values in `.env`:
246
549
 
247
- ```bash
550
+ ```dotenv
248
551
  HANDOFF_FIGMA_PROJECT_ID=figma-file-id
249
552
  HANDOFF_DEV_ACCESS_TOKEN=figma-personal-access-token
250
553
  ```
251
554
 
252
- The `file_content:read` and `library_content:read` scopes must be granted to the
253
- 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:
254
557
 
255
558
  ```bash
256
559
  npm run fetch
257
560
  ```
258
561
 
259
- After design changes, the Figma library should be republished and the command
260
- 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.
261
574
 
262
- Generated tokens, CSS, Sass, and available assets are written below
263
- `exported/<projectId>`. The `exported/` directory should be committed so the
264
- same inputs are used by local and CI builds. A successful fetch can be verified
265
- by checking for generated token data such as `exported/<projectId>/tokens.json`;
266
- an empty foundation route does not prove that the fetch succeeded.
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:
578
+
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
+ ```
267
587
 
268
- ## Workspace
588
+ ## Running the workspace
269
589
 
270
- Components, patterns, pages, styles, and configuration are authored as local
271
- workspace files. For day-to-day authoring, the documentation server and file
272
- watchers are started with:
590
+ Start the documentation server and the Handoff file watchers:
273
591
 
274
592
  ```bash
275
593
  npm run start
276
594
  ```
277
595
 
278
- Changes are rebuilt and reflected in the documentation while the command is
279
- running. When Handoff file watchers are not required, the development server
280
- 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.
281
599
 
282
- The following should be visible at http://localhost:3000:
600
+ Open http://localhost:3000 and make sure that:
283
601
 
284
- - the app reports `Workspace` as the active runtime mode;
285
- - components appear in the component list;
286
- - component detail pages render every configured preview;
287
- - patterns render their referenced components; and
288
- - 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.
289
607
 
290
- ## Build outputs
608
+ ## Building
291
609
 
292
- A static documentation site is built with:
610
+ **Static site.** Build the documentation as a static site:
293
611
 
294
612
  ```bash
295
613
  npm run build
296
614
  ```
297
615
 
298
- Static output is written below `out/<projectId>` and can be served by any
299
- 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.
300
619
 
301
- 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:
302
626
 
303
627
  ```bash
304
- npm run build -- --target registry
628
+ npm run build -- --force
305
629
  ```
306
630
 
307
- The generated application is automatically configured for registry mode by the
308
- command. The database-backed Next.js bundle is written to `out/registry`, and
309
- workspace source is not compiled or served. A Vercel Build Output bundle can be
310
- produced for either target by adding `--package vercel`.
631
+ **Registry server.** Build a standalone registry application:
311
632
 
312
- ## Registry setup
633
+ ```bash
634
+ npm run build -- --target registry
635
+ ```
313
636
 
314
- ### 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.
315
640
 
316
- Migrations are run from the source workspace or CI checkout, where the project
317
- dependency and configuration are available:
641
+ **Vercel.** Add `--package vercel` to either target to make a Vercel Build
642
+ Output bundle in `.vercel/output`:
318
643
 
319
644
  ```bash
320
- DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public" \
321
- npm run db:migrate
645
+ npm run build -- --target registry --package vercel
322
646
  ```
323
647
 
324
- Migrations are a controlled release step and are separate from the build,
325
- server startup, and browser installer.
648
+ ## Registry
326
649
 
327
- ### 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.
328
653
 
329
- The generated registry bundle uses these environment variables:
654
+ ### Deploy a registry
330
655
 
331
- ```dotenv
332
- PORT=4000
333
- HOSTNAME=0.0.0.0
334
- DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public"
335
- AUTH_SECRET="replace-with-at-least-32-random-characters"
336
- AUTH_URL="http://localhost:4000"
337
- ```
656
+ 1. Apply the database migrations from the workspace or a CI checkout, where the
657
+ project config is available:
338
658
 
339
- `DATABASE_URL`, `AUTH_SECRET`, and `AUTH_URL` are required. The standalone
340
- server also reads `PORT` and `HOSTNAME`. The generated entrypoint is
341
- `out/registry/server.js`; how it is started and hosted depends on the deployment
342
- environment.
659
+ ```bash
660
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/handoff?schema=public" \
661
+ npm run db:migrate
662
+ ```
343
663
 
344
- ### 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.
345
667
 
346
- Vercel packaging and the Neon PostgreSQL driver are supported independently.
347
- 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.
348
671
 
349
- #### Vercel packaging
672
+ 3. Build the registry:
350
673
 
351
- A registry bundle that follows the Vercel Build Output API is produced with:
674
+ ```bash
675
+ npm run build -- --target registry
676
+ ```
352
677
 
353
- ```bash
354
- npm run build -- --target registry --package vercel
355
- ```
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
+ ```
687
+
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.
356
691
 
357
- The generated deployment output is written to `.vercel/output`. The registry
358
- runtime variables listed above are supplied through the deployment environment.
692
+ 5. Open http://localhost:4000/install and create the first administrator. The
693
+ installer examines the deployment, but it never runs migrations.
359
694
 
360
- #### Neon PostgreSQL
695
+ CAUTION: Complete the installation immediately after the deployment. The first
696
+ visitor can claim a registry that is not installed.
361
697
 
362
- Import `fromEnv` from `handoff-app`. Select the Neon connection driver in `handoff.config.ts`:
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:
363
705
 
364
706
  ```ts
365
707
  runtime: {
366
708
  registry: {
367
- databaseUrl: fromEnv('DATABASE_URL'),
368
709
  database: {
710
+ url: fromEnv('DATABASE_URL'),
369
711
  driver: 'neon',
370
712
  },
371
713
  },
372
714
  },
373
715
  ```
374
716
 
375
- A Neon PostgreSQL connection string is supplied through `DATABASE_URL`. The
376
- same explicit database migration step is required before deployment.
717
+ Both drivers use the same schema and the same `db:migrate` step.
377
718
 
378
- #### Optional Vercel Blob storage
719
+ ### Asset storage
379
720
 
380
- Registry assets are stored in PostgreSQL by default. Vercel Blob can be selected
381
- when external asset storage is required:
721
+ By default, the registry stores asset blobs in PostgreSQL. Each blob can be at
722
+ most 4 MB. `assetStorage.maxInlineBytes` changes this limit.
723
+
724
+ A [custom server module](#custom-server-modules) can store blobs in a
725
+ different service:
382
726
 
383
727
  ```ts
384
728
  runtime: {
385
729
  registry: {
386
730
  assetStorage: {
387
- adapter: 'vercel-blob',
731
+ provider: 'custom',
732
+ module: './server/storage/s3.mjs',
733
+ options: { bucket: 'handoff-assets', secretKey: fromEnv('S3_SECRET_KEY') },
734
+ },
735
+ },
736
+ },
737
+ ```
738
+
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 |
756
+
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.
760
+
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.
766
+
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.
770
+
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.
777
+
778
+ > **Recipes:** [S3-compatible storage](RECIPES.md#s3-compatible-storage) ·
779
+ > [Vercel Blob](RECIPES.md#vercel-blob)
780
+
781
+ ### Email
782
+
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') },
388
794
  },
389
795
  },
390
796
  },
391
797
  ```
392
798
 
393
- The associated `BLOB_READ_WRITE_TOKEN` is supplied through the deployment
394
- environment.
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)
395
813
 
396
- ### 3. Installation
814
+ ### Custom server modules
397
815
 
398
- The installer is opened at http://localhost:4000/install, where the first
399
- administrator is created. The deployment is verified by the installer, but
400
- migrations are never run by it.
816
+ Asset storage, email, and AI connections can name a custom server module. Each
817
+ module must obey these rules:
401
818
 
402
- An uninstalled registry can be claimed by the first visitor. A new deployment
403
- should not be left unattended before installation is complete.
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.
404
826
 
405
- ### 4. Workspace authorization
827
+ The factory gets `options` with each `fromEnv()` value resolved at request
828
+ time.
406
829
 
407
- A workspace reaches a registry with a registry URL and an access token. There
408
- are two ways to supply them. Both use the same kind of token, and both are
409
- listed and revoked under Account → Access tokens in the registry.
830
+ ## Connecting a workspace to a registry
410
831
 
411
- #### Device login
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.
412
837
 
413
- CLI authorization is started from the source workspace with:
838
+ ### Device login
839
+
840
+ Start the authorization from the workspace:
414
841
 
415
842
  ```bash
416
843
  npm run login -- --url http://localhost:4000
417
844
  ```
418
845
 
419
- Registry sign-in, entry of the displayed device code, and CLI approval are
420
- completed in the browser. The issued credential is saved in
421
- `.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.
422
849
 
423
- One credential is saved per profile, so a workspace can stay signed in to
424
- 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:
425
852
 
426
853
  ```bash
427
854
  npm run login -- --profile staging --url https://staging.example.com
@@ -430,44 +857,55 @@ npm run publish -- all --profile staging
430
857
 
431
858
  A login profile name is free. It needs no `handoff.config.<name>.*` file, and
432
859
  `login` is the only command that accepts a new name. A profile without its own
433
- login falls back to the default login, so one `npm run login` is still enough
434
- 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.
435
862
 
436
- `npm run logout` revokes and removes the login of the selected profile. The
437
- `--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.
438
865
 
439
- #### Environment variables
866
+ ### Access token
440
867
 
441
- A token is created in the registry under Account → Access tokens. Read and
442
- write access is required to publish; read access is enough to check out. The
443
- 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`:
444
871
 
445
872
  ```dotenv
446
873
  HANDOFF_REGISTRY_URL=http://localhost:4000
447
874
  HANDOFF_REGISTRY_ACCESS_TOKEN=hnd_...
448
875
  ```
449
876
 
450
- No configuration file entry is needed, because these two variables are the
451
- defaults for `runtime.registryConnection`. A selected profile reads
452
- `.env.<profile>` on top of `.env`, so one workspace can address a different
453
- registry per profile. In CI, the job environment supplies the same two
454
- 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.
881
+
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.
887
+
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:
455
890
 
456
- Environment values win over a saved device login, so a CI job stays
457
- deterministic on a machine where a developer is signed in. A token is only used
458
- for the registry URL it was issued for. If the environment names a different
459
- registry than the login of the selected profile, publish and checkout report
460
- which login was skipped and why.
891
+ ```ts
892
+ runtime: {
893
+ registryConnection: {
894
+ url: 'https://registry.example.com',
895
+ accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN'),
896
+ },
897
+ },
898
+ ```
461
899
 
462
- ### 5. Content publishing
900
+ ### Publishing
463
901
 
464
- Every kind in the workspace is published in dependency order with:
902
+ Publish every kind of content in dependency order:
465
903
 
466
904
  ```bash
467
905
  npm run publish -- all
468
906
  ```
469
907
 
470
- A single kind is published on its own:
908
+ Publish one kind:
471
909
 
472
910
  ```bash
473
911
  npm run publish -- catalog
@@ -476,201 +914,186 @@ npm run publish -- tokens
476
914
  npm run publish -- assets
477
915
  ```
478
916
 
479
- `catalog` covers every catalog item. An item is stored as a component or as a
480
- pattern, and its declaration decides which. The command names items and never a
481
- 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.
482
920
 
483
- Publishing tokens or assets runs the Figma data pipeline before upload, so the
484
- 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.
485
923
 
486
- 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:
487
925
 
488
926
  ```bash
489
927
  npm run publish -- catalog item-id another-id
490
928
  ```
491
929
 
492
- `--dry-run` reports what would be uploaded and contacts no registry at all, so
493
- it needs neither a registry URL nor a token. It still runs the build, which
494
- refreshes generated output on disk; `--no-build` skips the build and publishes
495
- 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:
496
935
 
497
936
  ```bash
498
937
  npm run publish -- all --dry-run
499
938
  npm run publish -- catalog --no-build
500
939
  ```
501
940
 
502
- `checkout` takes the same `all`, multi-ID, and `--dry-run` forms. A dry-run
503
- checkout reads from the registry, lists the files it would create or overwrite,
504
- and writes nothing.
941
+ After a publish, reload the registry to see the published items and
942
+ foundations.
505
943
 
506
- After the registry is reloaded, the published catalog items and foundations are
507
- visible. Published database records are read by registry pages; the local
508
- workspace is never read directly.
944
+ ### Checkout
509
945
 
510
- For CI, supply `HANDOFF_REGISTRY_URL` and `HANDOFF_REGISTRY_ACCESS_TOKEN`
511
- through the job environment, as described under
512
- [Workspace authorization](#4-workspace-authorization). A connection block in
513
- `handoff.config.ts` is needed only to pin the URL in the repository, or to read
514
- the values from differently named variables (`fromEnv` is imported from
515
- `handoff-app`):
946
+ `checkout` writes registry content into the workspace. It takes the same
947
+ `all`, kind, id, and `--dry-run` forms as `publish`:
516
948
 
517
- ```ts
518
- runtime: {
519
- registryConnection: {
520
- url: 'https://registry.example.com',
521
- accessToken: fromEnv('HANDOFF_REGISTRY_ACCESS_TOKEN'),
522
- },
523
- },
949
+ ```bash
950
+ npm run checkout -- all
951
+ npm run checkout -- catalog item-id
952
+ npm run checkout -- all --dry-run
524
953
  ```
525
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
+
526
962
  ## MCP
527
963
 
528
- The documentation app serves a Model Context Protocol endpoint at `/api/mcp/`, so
529
- coding agents can look up the pages, components and tokens that already exist instead
530
- of inventing markup and values. It is stateless Streamable HTTP over `POST`;
531
- there is nothing to start and no extra port. Keep the trailing slash: the app
532
- 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.
533
968
 
534
969
  | Target | Endpoint | Credential |
535
970
  | --- | --- | --- |
536
- | Workspace (`dev` / `start`) | `http://localhost:3000/api/mcp/` | none, as with `/api/docs/*` |
537
- | Registry (standalone or Vercel) | `<registry url>/api/mcp/` | an access token, read-only is enough |
538
- | 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.
539
985
 
540
- Registry tokens come from `handoff-app login` or Account settings in the
541
- 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)
542
988
 
543
- 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:
544
998
 
545
999
  ```ts
546
1000
  runtime: {
547
- 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
+ },
548
1013
  },
549
1014
  ```
550
1015
 
551
- Like `runtime.mode`, this is config-only and baked at build time, so changing it
552
- in a deployed registry means a rebuild. A disabled build answers 404 on the route,
553
- drops the MCP SDK from the packaged bundle, and shows no connect affordance at all
554
- 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.
555
1018
 
556
- The documentation app carries its own endpoint and a ready-made client config
557
- for Claude Code, Cursor and VS Code behind the plug icon in the header. By hand:
1019
+ Each connection uses one of these credentials:
558
1020
 
559
- ```json
560
- {
561
- "mcpServers": {
562
- "handoff": {
563
- "type": "http",
564
- "url": "https://registry.example.com/api/mcp/",
565
- "headers": { "Authorization": "Bearer hnd_..." }
566
- }
567
- }
568
- }
569
- ```
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.
570
1025
 
571
- ## 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.
572
1030
 
573
- Configuration is read from `handoff.config.ts`, `.js`, `.cjs`, or `.json`, in
574
- that order. `defineConfig` provides typed authoring. Values merge onto the
575
- defaults: plain objects merge recursively, and arrays, scalars, `null`, and
576
- 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`.
577
1037
 
578
- 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.
579
1041
 
580
- | Variable | Purpose |
581
- | --- | --- |
582
- | `HANDOFF_PROFILE` | Config profile merged onto the base config |
583
- | `HANDOFF_FIGMA_PROJECT_ID` | Figma file ID used by `fetch` |
584
- | `HANDOFF_DEV_ACCESS_TOKEN` | Figma personal access token used by `fetch` |
585
- | `HANDOFF_REGISTRY_URL` | Connected workspace registry URL |
586
- | `HANDOFF_REGISTRY_ACCESS_TOKEN` | Registry access token used by a connected workspace |
587
- | `HANDOFF_SYNC_SECRET` | Optional deployment-wide registry credential |
588
- | `DATABASE_URL` | Registry PostgreSQL connection string |
589
- | `AUTH_SECRET` | Registry session-signing secret, at least 32 characters |
590
- | `AUTH_URL` | Canonical public registry URL |
591
- | `PORT` | Standalone registry server port |
592
- | `HOSTNAME` | Standalone registry bind hostname |
593
- | `BLOB_READ_WRITE_TOKEN` | Credential for the Vercel Blob asset adapter |
594
- | `HANDOFF_OUTPUT_DIR` | Override the fetched output directory |
595
- | `HANDOFF_SITES_DIR` | Override the build output directory |
596
- | `HANDOFF_APP_PORT` | Workspace documentation server port |
597
- | `HANDOFF_WEBSOCKET_PORT` | Workspace live-reload server port |
598
-
599
- Registry assets are stored in PostgreSQL by default with a 4 MB per-blob limit.
600
- Vercel Blob or a custom adapter can be selected through
601
- `runtime.registry.assetStorage`. The `pg` or `neon` connection driver can be
602
- selected through `runtime.registry.database.driver`; PostgreSQL is used by
603
- both.
604
-
605
- ## Profiles
606
-
607
- A profile is a sidecar config file that merges onto the base config. It holds
608
- only what changes between environments, so a project keeps one shared config
609
- instead of a second complete copy of it.
610
-
611
- - The base config is `handoff.config.ts`, `.js`, `.cjs`, or `.json`. It is the
612
- only config file loaded when no profile is selected.
613
- - A profile file is `handoff.config.<profile>.*`, with the same four
614
- extensions.
615
- - Select a profile with `--profile <name>` or with `HANDOFF_PROFILE`.
616
- `--profile` takes precedence over `HANDOFF_PROFILE`.
617
- - Profile names are not predefined. A name can contain lowercase letters,
618
- numbers, and hyphens.
619
- - A selected profile must exist. If no `handoff.config.<name>.*` file is found,
620
- the command stops with an error.
621
- - `login`, `logout`, `publish`, and `checkout` also accept a profile that has
622
- only a saved registry login, because there the profile selects the registry.
623
- Every other command still needs the config file.
624
- - Layers resolve as defaults, base config, profile, then programmatic config,
625
- with the merge rules described under [Configuration](#configuration).
626
- - A selected profile also reads `.env.<profile>` on top of `.env`, when that
627
- file exists. Both are read from the directory the command runs in, and a
628
- variable already set in the environment beats both files.
1042
+ ### Reader keys
629
1043
 
630
- ```bash
631
- npx handoff-app build --target registry --profile registry
632
- ```
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`.
633
1053
 
634
- `build --target registry` always packages a registry-mode application, so a
635
- profile does not need to set `runtime.mode`. This makes a profile a good place
636
- for the registry build settings: the database driver, the database
637
- environment-variable name, the asset storage, and whether MCP is enabled. A
638
- hosting provider or a CI job selects the profile through `HANDOFF_PROFILE`.
1054
+ ### Changing connections without a rebuild
639
1055
 
640
- `defineConfig` types a profile as well as a base config. The generated
641
- `.gitignore` lists `handoff.config.local.*`, which makes `local` the usual name
642
- 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.
643
1060
 
644
- Arrays replace, so a profile that declares `catalog.include` also decides what
645
- `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.
646
1063
 
647
- `.env.<profile>` is read once at startup rather than watched.
648
- `HANDOFF_WORKING_PATH` cannot be set from it, because the working path is
649
- 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)
650
1068
 
651
1069
  ## CLI reference
652
1070
 
653
1071
  | Command | Description |
654
1072
  | --- | --- |
655
- | `npx handoff-app init` | A workspace is scaffolded with the interactive wizard |
656
- | `npm run fetch` | Design tokens and assets are fetched from Figma |
657
- | `npm run start` | The workspace server and Handoff file watchers are started |
658
- | `npm run dev` | The workspace development server is started |
659
- | `npm run build -- [--target static\|registry]` | The static site or standalone registry bundle is built |
660
- | `npm run db:migrate` | Registry database migrations are applied |
661
- | `npm run validate` | Configured components are validated |
662
- | `npm run publish -- <kind\|all> [id...]` | Catalog items, pages, tokens, or assets are published |
663
- | `npm run checkout -- <kind\|all> [id...]` | Published content is pulled into a workspace |
664
- | `npm run login -- [--profile <name>] --url <url>` | The CLI is authorized through the registry device flow |
665
- | `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 |
666
1088
 
667
1089
  Every command except `init` accepts `-c, --config`, `--profile`, `-d, --debug`,
668
- and `-f, --force`. `publish` and `checkout` additionally accept `--dry-run`, and
1090
+ and `-f, --force`. `publish` and `checkout` also accept `--dry-run`, and
669
1091
  `publish` accepts `--no-build`.
670
1092
 
671
- Arguments after `--` are forwarded to the local CLI. Exact options can be shown
672
- by adding `--help` after the separator. Advanced configuration and hooks are
673
- 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.
674
1097
 
675
1098
  ## Maintainers
676
1099
 
@@ -690,4 +1113,4 @@ Code of Conduct.
690
1113
 
691
1114
  ## License
692
1115
 
693
- [MIT](License.md) ©Convertiv
1116
+ [MIT](LICENSE.md) ©Convertiv